diff --git a/extra/admin-api/SpacebarAdminAPI.slnx b/extra/admin-api/SpacebarAdminAPI.slnx index e2ec0b008..9a6115ee3 100644 --- a/extra/admin-api/SpacebarAdminAPI.slnx +++ b/extra/admin-api/SpacebarAdminAPI.slnx @@ -1,42 +1,44 @@ - - - + + + - - + + - - - - - - - + + + + + + + - - - - - + + + + + + - - - - - - - - + + + + + + + + + - - - - - + + + + + diff --git a/extra/admin-api/Utilities/Spacebar.Client/App.razor b/extra/admin-api/Utilities/Spacebar.Client/App.razor new file mode 100644 index 000000000..9a3f10935 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/App.razor @@ -0,0 +1,18 @@ +@using Spacebar.Client.Components + + + + + + + + + + + + +@code { + public static SessionManager SessionManager { get; set; } = null!; + public static ClientManager ClientManager { get; set; } = null!; + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor b/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor new file mode 100644 index 000000000..71e82698d --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor @@ -0,0 +1,61 @@ +@using ArcaneLibs.Blazor.Components.Services +@using ArcaneLibs.Extensions +@using Spacebar.Client.Core +@using Spacebar.Client.WebCore +@using Spacebar.Models.Gateway +@inject SessionStore sessionStore +@inject SpacebarClientProviderService clientProvider +@inject JsConsoleService jsConsole + + + + @ChildContent + + +@code { + private DebugBanner _dbgBanner = null!; + private AuthenticatedSpacebarClient? _client { get; set; } + + [Parameter] + public required RenderFragment ChildContent { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (!firstRender) return; + await _dbgBanner.SetStatus("Preparing for launch!"); + await Task.Delay(125); + var session = await sessionStore.GetCurrentSessionAsync(); + if (session != null) { + _client = await clientProvider.GetAuthenticatedClientAsync(session.ServerName, session.AccessToken); + await _dbgBanner.SetStatus($"Got authenticated client for {session.ProfileCache.Username}#{session.ProfileCache.Discriminator} on {session.ServerName}! Connecting to gateway..."); + _client.Gateway.IdentifyData.ClientProperties = new IdentifyClientProperties() { + HasClientMods = false, + ApplicationArchitecture = "wasm" + }.ToJsonNode().AsObject(); + StateHasChanged(); + await _client.Gateway.Connect(); + _ = _client.Gateway.Start().ContinueWith(ct => { + jsConsole.Warn("[ClientManager] Heartbeat loop exited!"); + if (ct.IsFaulted) { + jsConsole.Error("Unhandled exception during gateway connection:", ct.Exception.ToString()); + throw ct.Exception; + } + }); + _client.Gateway.OnceGatewayMessage.Add(async msg => { + if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) { + await _dbgBanner.SetStatus($"Got READY from gateway"); + await _dbgBanner.SetStatus(null, 1750); + var content = msg.GetData(); + await jsConsole.Info("Parsed ready payload:", content); + return true; + } + return false; + }); + } + else { + await _dbgBanner.SetStatus("No session marked as current... :("); + await _dbgBanner.SetStatus(null, 1750); + } + + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/DebugBanner.razor b/extra/admin-api/Utilities/Spacebar.Client/Components/DebugBanner.razor new file mode 100644 index 000000000..4cc627817 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/DebugBanner.razor @@ -0,0 +1,23 @@ +@(Name): @_status + +@code { + + private bool _bannerVisible = true; + private string? _status = "initializing..."; + + [Parameter] + public required string Name { get; set; } + + public async Task SetStatus(string? message, int closeDelay = 0) { + _bannerVisible = !string.IsNullOrWhiteSpace(message); + if (!_bannerVisible) { + await Task.Delay(closeDelay); + StateHasChanged(); + await Task.Delay(1000); + } + + _status = message; + StateHasChanged(); + await Task.Yield(); + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/SessionManager.razor b/extra/admin-api/Utilities/Spacebar.Client/Components/SessionManager.razor new file mode 100644 index 000000000..0c9a10644 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/SessionManager.razor @@ -0,0 +1,52 @@ +@using ArcaneLibs.Blazor.Components +@using ArcaneLibs.Blazor.Components.Services +@using Spacebar.Client.WebCore +@inject SessionStore sessionStore +@inject JsConsoleService jsConsole + + +@if (_sessionPickerVisible) { + + @foreach (var (sessionId, session) in _sessionEntries) { + @(session.ProfileCache?.Username ?? "Unknown user")#@(session.ProfileCache?.Discriminator ?? "0000") on @session.ServerName + } +

+ Log in + Register +
+} + +@code { + private bool _sessionPickerVisible; + private DebugBanner _dbgBanner; + + private Dictionary _sessionEntries = []; + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (!firstRender) return; + await _dbgBanner.SetStatus("Crunching numbers..."); + _sessionPickerVisible = await sessionStore.GetCurrentSessionAsync() == null; + await _dbgBanner.SetStatus("Ready!"); + await _dbgBanner.SetStatus(null, 1000); + } + + public async Task SetSessionPickerVisible(bool visible = true) { + await jsConsole.Info("[SessionManager] showing session picker..."); + _sessionEntries = visible ? await sessionStore.GetAllSessionsAsync() : []; + _sessionPickerVisible = visible; + StateHasChanged(); + await Task.Delay(1); + } + + private async Task SetCurrentSessionAsync(Guid sessionId) { + _sessionPickerVisible = false; + StateHasChanged(); + await sessionStore.SetCurrentSessionAsync(sessionId); + var cs = await sessionStore.GetCurrentSessionAsync(); + await _dbgBanner.SetStatus($"Current session changed to {cs!.ProfileCache?.Username}#{cs.ProfileCache?.Discriminator} ({cs.ProfileCache.Id}) / {sessionId.ToString()}!"); + await _dbgBanner.SetStatus(null, 3000); + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs new file mode 100644 index 000000000..f8d903096 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs @@ -0,0 +1,164 @@ +using System.Data.Common; +using System.Diagnostics; +using System.Net.Http.Json; +using System.Net.WebSockets; +using System.Text.Json; +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using Spacebar.Models.Api; +using Spacebar.Models.Gateway; +using Spacebar.Models.Generic; + +namespace Spacebar.Client.Core; + +public class UnauthenticatedSpacebarClient(ILogger logger, SpacebarClientWellKnown wellKnown) { + public async Task LoginAsync(LoginRequest request) { + // TODO: rebase + using var hc = new HttpClient(); + var resp = await hc.PostAsJsonAsync(new Uri(wellKnown.Api.GetApiBaseUrl(), "auth/login"), request); + // TODO: abstract out + if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync())!); + return (await resp.Content.ReadFromJsonAsync())!; + } +} + +public class AuthenticatedSpacebarClient { + private readonly ILogger _logger; + + public AuthenticatedSpacebarClient(ILogger logger, IServiceProvider sp, SpacebarClientWellKnown wellKnown, string token) { + _logger = logger; + ApiHttpClient = new HttpClient() { + BaseAddress = wellKnown.Api.GetApiBaseUrl() + }; + ApiHttpClient.DefaultRequestHeaders.Authorization = new("Bearer", token); + Gateway = new(sp.GetRequiredService>(), wellKnown, token); + } + + public HttpClient ApiHttpClient { get; set; } + public AuthenticatedSpacebarGatewayClient Gateway { get; set; } + + // TODO: write a proper full user model... + public async Task GetCurrentUser() { + var resp = await ApiHttpClient.GetAsync("users/@me"); + // TODO: abstract out + if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync())!); + return (await resp.Content.ReadFromJsonAsync())!; + } + + ~AuthenticatedSpacebarClient() { + ApiHttpClient.Dispose(); + } +} + +public class AuthenticatedSpacebarGatewayClient(ILogger logger, SpacebarClientWellKnown wellKnown, string token) { + public ClientWebSocket RawClientWebSocket = new(); + public int Sequence; + public bool TraceGatewayMessages = false; + + public IdentifyRequest IdentifyData { get; } = new() { + Intents = (GatewayIntentFlags?)0xFFFFFFFF, // too lazy to do math, just gimme everything + Capabilities = 0 + }; + + public List> OnGatewayMessage { get; } = []; + public List>> OnceGatewayMessage { get; } = []; + + public async Task Connect() { + if (RawClientWebSocket.State is WebSocketState.Connecting or WebSocketState.Open) return; + Sequence = 0; + await RawClientWebSocket.ConnectAsync(new Uri(wellKnown.Gateway.BaseUrl).AddQuery("encoding", "json"), CancellationToken.None); + } + + public async Task Start() { + await foreach (var msg in _runReceiveLoop()) { + // logger.LogInformation("Got gateway message: {msg}", msg); + if (msg.Opcode == GatewayOpcode.S2CHello) { + _ = _runHeartbeatLoop(msg.GetData()!.HeartbeatInterval).ContinueWith(ct => { + logger.LogWarning("Heartbeat loop exited!"); + if (ct.IsFaulted) throw ct.Exception; + }); + IdentifyData.Token = token; + await RawClientWebSocket.SendAsync(JsonSerializer.SerializeToUtf8Bytes(new GatewayPayload() { + Opcode = GatewayOpcode.C2SIdentify, // Identify + EventData = IdentifyData.ToJsonNode().AsObject() + }), WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + } + else if (msg.Opcode == GatewayOpcode.S2CHeartbeatAck) { + logger.LogInformation("Got heartbeat ACK from server!"); + } + + await Task.WhenAll(OnGatewayMessage.Select(x => x(msg)).ToArray()); + foreach (var t in OnceGatewayMessage.Select((Func> Callback, Task WasHandled) (cb) => (cb, cb(msg))).ToList()) { + var handled = await t.WasHandled; + if (handled) { + OnceGatewayMessage.Remove(t.Callback); + } + } + } + } + + private async Task _runHeartbeatLoop(int interval) { + while (RawClientWebSocket.State < WebSocketState.Closed) { + await RawClientWebSocket.SendAsync(JsonSerializer.SerializeToUtf8Bytes(new GatewayPayload() { + Opcode = GatewayOpcode.C2SQoSHeartbeat, // QoS Heartbeat + EventData = new QoSHeartbeatRequest() { + Sequence = Sequence, + QoSPayload = new() { + Active = true, + Reasons = ["foregrounded"], + Version = 27 + } + }.ToJsonNode().AsObject() + }), WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + await Task.Delay(interval); + } + } + + private const int ReceiveBufferSize = 2 * 1024 * 1024; + + private async IAsyncEnumerable _runReceiveLoop() { + List messageParts = []; + List<(string Name, TimeSpan Elapsed)> trace = []; + var buffer = new byte[ReceiveBufferSize]; + int idx = 0; + + while (RawClientWebSocket.State < WebSocketState.Closed) { + var sw = Stopwatch.StartNew(); + + var msg = await RawClientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + trace.Add(($"RCV.{idx}", sw.GetElapsedAndRestart())); + + Console.WriteLine($"Websocket message chunk read: {msg.MessageType} {msg.Count} {msg.EndOfMessage}"); + messageParts.AddRange(msg.Count < ReceiveBufferSize ? buffer[..msg.Count] : buffer); + trace.Add(($"STO.{idx}({msg.Count})", sw.GetElapsedAndRestart())); + idx++; + + if (msg.EndOfMessage) { + Console.WriteLine("Got message, deserialising..."); + var fullMsg = messageParts.ToArray(); + trace.Add(($"LD({messageParts.Count})", sw.GetElapsedAndRestart())); + + var d = JsonSerializer.Deserialize(fullMsg); + trace.Add(($"LJS({fullMsg.Length})", sw.GetElapsedAndRestart())); + + yield return d ?? throw new InvalidDataException("Gateway message deserialisation returned null?"); + trace.Add(($"YLD", sw.GetElapsedAndRestart())); + + if (TraceGatewayMessages) { + Console.WriteLine("Yielded message!"); + Console.WriteLine("Trace:"); + foreach (var t in trace) + Console.WriteLine($" - {t.Name}: {t.Elapsed}"); + } + + messageParts.Clear(); + trace.Clear(); + trace.Add(($"RST", sw.GetElapsedAndRestart())); + } + } + } + + ~AuthenticatedSpacebarGatewayClient() { + RawClientWebSocket.Dispose(); + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClientProviderService.cs b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClientProviderService.cs new file mode 100644 index 000000000..a732c9a5f --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClientProviderService.cs @@ -0,0 +1,26 @@ +using ArcaneLibs.Collections; + +namespace Spacebar.Client.Core; + +public class SpacebarClientProviderService(ILogger logger, IServiceProvider serviceProvider, SpacebarClientWellKnownResolverService clientWellKnownResolver) { + private static readonly SemaphoreCache UnauthenticatedClientCache = new(); + private static readonly SemaphoreCache AuthenticatedClientCache = new(); + + public async Task GetUnauthenticatedClientAsync(string serverName) { + return await UnauthenticatedClientCache.GetOrAdd(serverName, async () => { + logger.LogInformation("Creating a new unauthenticated client for {serverName}!", serverName); + var clientLogger = serviceProvider.GetRequiredService>(); + var wellKnown = await clientWellKnownResolver.ResolveClientWellKnown(serverName); + return new UnauthenticatedSpacebarClient(clientLogger, wellKnown); + }); + } + + public async Task GetAuthenticatedClientAsync(string serverName, string accessToken) { + return await AuthenticatedClientCache.GetOrAdd(serverName, async () => { + logger.LogInformation("Creating a new authenticated client for {serverName}!", serverName); + var clientLogger = serviceProvider.GetRequiredService>(); + var wellKnown = await clientWellKnownResolver.ResolveClientWellKnown(serverName); + return new AuthenticatedSpacebarClient(clientLogger, serviceProvider, wellKnown, accessToken); + }); + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClientWellKnownResolver.cs b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClientWellKnownResolver.cs new file mode 100644 index 000000000..a106435bc --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClientWellKnownResolver.cs @@ -0,0 +1,57 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; + +namespace Spacebar.Client.Core; + +public class SpacebarClientWellKnownResolverService(ILogger logger) { + public async Task ResolveClientWellKnown(string serverName) { + using var hc = new HttpClient(); + logger.LogInformation("Resolving .well-known for {serverName}", serverName); + return await hc.GetFromJsonAsync($"https://{serverName}/.well-known/spacebar/client")!; + } +} + +public class SpacebarClientWellKnown { + [JsonPropertyName("api")] + public required ApiWellKnownData Api { get; set; } + + [JsonPropertyName("cdn")] + public required GenericUrlWellKnownData Cdn { get; set; } + + [JsonPropertyName("admin")] + public GenericUrlWellKnownData? Admin { get; set; } + + [JsonPropertyName("gateway")] + public required GatewayWellKnownData Gateway { get; set; } + + public class GenericUrlWellKnownData { + [JsonPropertyName("baseUrl")] + public required string BaseUrl { get; set; } + } + + public class ApiWellKnownData : GenericUrlWellKnownData { + [JsonPropertyName("apiVersions")] + public required ApiVersionsData ApiVersions { get; set; } + + // Utility methods + public Uri GetApiBaseUrl(string? version = null) { + return new Uri(BaseUrl + "/api/v" + (version ?? ApiVersions.Default)); + } + + public class ApiVersionsData { + [JsonPropertyName("default")] + public required string Default { get; set; } + + [JsonPropertyName("active")] + public required List Active { get; set; } + } + } + + public class GatewayWellKnownData : GenericUrlWellKnownData { + [JsonPropertyName("encoding")] + public required List Encoding { get; set; } + + [JsonPropertyName("compression")] + public required List Compression { get; set; } + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Layout/MainLayout.razor b/extra/admin-api/Utilities/Spacebar.Client/Layout/MainLayout.razor new file mode 100644 index 000000000..e7554be8d --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Layout/MainLayout.razor @@ -0,0 +1,16 @@ +@inherits LayoutComponentBase +
+ + +
+
+ About +
+ +
+ @Body +
+
+
\ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Layout/MainLayout.razor.css b/extra/admin-api/Utilities/Spacebar.Client/Layout/MainLayout.razor.css new file mode 100644 index 000000000..ecf25e5b2 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Layout/MainLayout.razor.css @@ -0,0 +1,77 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor new file mode 100644 index 000000000..ccc73abe4 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor @@ -0,0 +1,39 @@ + + + + +@code { + private bool collapseNavMenu = true; + + private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() { + collapseNavMenu = !collapseNavMenu; + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css new file mode 100644 index 000000000..617b89cc8 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css @@ -0,0 +1,83 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } + + .nav-scrollable { + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/Auth/Login.razor b/extra/admin-api/Utilities/Spacebar.Client/Pages/Auth/Login.razor new file mode 100644 index 000000000..a7ab18286 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Auth/Login.razor @@ -0,0 +1,92 @@ +@page "/login" +@using ArcaneLibs.Blazor.Components +@using ArcaneLibs.Extensions +@using Spacebar.Client.Core +@using Spacebar.Client.WebCore +@inject SpacebarClientWellKnownResolverService clientWellKnownResolver +@inject SessionStore sessionStore +@inject SpacebarClientProviderService clientProvider + +Log in + +Instance (server name): + +[🧭 Discover] +
+Email: + +
+Password: + +
+Log in + +
@ServerValidationStatus
+ +@code{ + + private string ServerName { + get; + set { + field = value; + CheckServer(value); + } + } = "spacebar.chat"; + + private string Username { get; set; } = ""; + private string Password { get; set; } = ""; + + private string? ServerValidationStatus; + + private async Task SetStatus(string? status) { + ServerValidationStatus = status; + StateHasChanged(); + await Task.Yield(); + } + + private async Task CheckServer(string serverName) { + await SetStatus("Checking server: " + serverName); + try { + await clientWellKnownResolver.ResolveClientWellKnown(serverName); + await SetStatus($"Server {serverName} is valid!"); + } + catch (Exception e) { + await SetStatus("Could not validate server: " + e.Message); + } + } + + private async Task LoginAsync() { + try { + await SetStatus($"Looking up {ServerName}..."); + var cwk = await clientWellKnownResolver.ResolveClientWellKnown(ServerName); + + await SetStatus($"Trying to log in to {ServerName}..."); + var usc = await clientProvider.GetUnauthenticatedClientAsync(ServerName); + var lrsp = await usc.LoginAsync(new() { + Login = Username, + Password = Password + }); + + await SetStatus($"Logged in as user ID {lrsp.UserId}, fetching profile..."); + var asc = await clientProvider.GetAuthenticatedClientAsync(ServerName, lrsp.Token!); + var cu = await asc.GetCurrentUser(); + await SetStatus($"Logged in as {cu.Username}#{cu.Discriminator} ({cu.Id})! Saving..."); + await sessionStore.AddSession(new SessionEntry() { + ServerName = ServerName, + AccessToken = lrsp.Token!, + ProfileCache = new() { + Id = cu.Id, + Username = cu.Username, + Discriminator = cu.Discriminator, + AvatarUrl = cu.Avatar ?? "", + } + }, setCurrent: true); + + // await SetStatus(lrsp.ToJson()); + } + catch (Exception e) { + await SetStatus("Failed to log in: " + e); + } + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor new file mode 100644 index 000000000..28709841d --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor @@ -0,0 +1,28 @@ +@* Workaround for Rider "Bad compile constant value" bug *@ +@attribute [Route(PageUri)] +@using ArcaneLibs.Blazor.Components.Services +@using Spacebar.Client.Core +@using Spacebar.Models.Gateway +@inject JsConsoleService jsConsole + +

@@me

+

@Client?.ApiHttpClient.BaseAddress

+

@Client?.Gateway.RawClientWebSocket.State

+ +@code { + private const string PageUri = "/channels/@me"; + + [Parameter, CascadingParameter] + public required AuthenticatedSpacebarClient? Client { + get; + set { field = value; Console.WriteLine("Set client: " + value); } + } + + protected override async Task OnParametersSetAsync() { + if (Client == null) return; + Client.Gateway.OnGatewayMessage.Add(async msg => { + await jsConsole.Log("Received gateway message: ", msg); + }); + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/Home.razor b/extra/admin-api/Utilities/Spacebar.Client/Pages/Home.razor new file mode 100644 index 000000000..fd39d3fc1 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Home.razor @@ -0,0 +1,26 @@ +@page "/" +@page "/app" +@using ArcaneLibs.Blazor.Components +@using ArcaneLibs.Extensions +@using Spacebar.Client.Core +@using Spacebar.Client.WebCore +@inject SpacebarClientWellKnownResolverService cswkrs +@inject SessionStore sessionStore + +Home + +

Hello, world!

+Trigger session picker +
@Ss.ToJson()
+
@Res.ToJson()
+ +@code{ + private SpacebarClientWellKnown? Res { get; set; } + private SessionEntry? Ss { get; set; } + + protected override async Task OnInitializedAsync() { + Ss = await sessionStore.GetCurrentSessionAsync(); + Res = await cswkrs.ResolveClientWellKnown(Ss?.ServerName ?? "spacebar.chat"); + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/NotFound.razor b/extra/admin-api/Utilities/Spacebar.Client/Pages/NotFound.razor new file mode 100644 index 000000000..917ada1d2 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/NotFound.razor @@ -0,0 +1,5 @@ +@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Program.cs b/extra/admin-api/Utilities/Spacebar.Client/Program.cs new file mode 100644 index 000000000..7d7d88e10 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Program.cs @@ -0,0 +1,19 @@ +using ArcaneLibs.Blazor.Components.Services; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Spacebar.Client; +using Spacebar.Client.Core; +using Spacebar.Client.WebCore; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Properties/launchSettings.json b/extra/admin-api/Utilities/Spacebar.Client/Properties/launchSettings.json new file mode 100644 index 000000000..77aa8afd4 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5086", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7015;http://localhost:5086", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj b/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj new file mode 100644 index 000000000..aae396d10 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj @@ -0,0 +1,39 @@ + + + + net10.0 + enable + enable + true + service-worker-assets.js + + + true + true + true + false + false + false + false + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/extra/admin-api/Utilities/Spacebar.Client/WebCore/SessionStore.cs b/extra/admin-api/Utilities/Spacebar.Client/WebCore/SessionStore.cs new file mode 100644 index 000000000..ae98ee570 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/WebCore/SessionStore.cs @@ -0,0 +1,61 @@ +using System.Text.Json.Serialization; +using ArcaneLibs.Blazor.Components.Services; + +namespace Spacebar.Client.WebCore; + +public class SessionStore(ILogger logger, LocalStorageService localStorage) { + public const string CurrentSessionKey = "chat.spacebar.client.current_session"; + public const string AllSessionsKey = "chat.spacebar.client.sessions"; + public async Task GetCurrentSessionAsync() { + if (!await localStorage.ContainsKeyAsync(CurrentSessionKey)) return null; + var entryId = await localStorage.GetItemFromJsonAsync(CurrentSessionKey); + var entries = await GetAllSessionsAsync(); + return entries[entryId]; + } + + public async Task> GetAllSessionsAsync() { + if (!await localStorage.ContainsKeyAsync(AllSessionsKey)) return []; + var data = await localStorage.GetItemFromJsonAsync>(AllSessionsKey); + return data ?? []; + } + + public async Task AddSession(SessionEntry sessionEntry, bool setCurrent = false) { + var sessions = await GetAllSessionsAsync(); + var newId = Guid.NewGuid(); + sessions.Add(newId, sessionEntry); + await localStorage.SetItemAsJsonAsync(AllSessionsKey, sessions); + + if (setCurrent) { + await localStorage.SetItemAsJsonAsync(CurrentSessionKey, newId); + } + } + + public async Task SetCurrentSessionAsync(Guid sessionId) { + await localStorage.SetItemAsJsonAsync(CurrentSessionKey, sessionId); + } +} + +public class SessionEntry { + [JsonPropertyName("server_name")] + public required string ServerName { get; set; } + + [JsonPropertyName("access_token")] + public required string AccessToken { get; set; } + + [JsonPropertyName("profile_cache")] + public required ProfileCacheData? ProfileCache { get; set; } + + public class ProfileCacheData { + [JsonPropertyName("id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public required long Id { get; set; } + + [JsonPropertyName("username")] + public required string Username { get; set; } + + [JsonPropertyName("discriminator")] + public string? Discriminator { get; set; } + + [JsonPropertyName("avatar_url")] + public required string AvatarUrl { get; set; } + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/_Imports.razor b/extra/admin-api/Utilities/Spacebar.Client/_Imports.razor new file mode 100644 index 000000000..551081647 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Spacebar.Client +@using Spacebar.Client.Layout \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css new file mode 100644 index 000000000..7c6e626e9 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css @@ -0,0 +1,171 @@ +@import url('jetbrains-mono/jetbrains-mono.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + background-color: #222; + color: #aaa; +} + +pre, .code { + font-family: 'JetBrains Mono', monospace; +} + +.dbgBanner { + transition: 0.25s; + display: block; + width: 100%; + transform: scale(1, 1); + height: 1.5em; +} + + .dbgBanner.hidden { + transition: 0.5s; + transform: scale(1, 0); + height: 0; + } + +#app > div > main > div { + background-color: #333; + border-bottom: none; +} + +.table, .table-striped > tbody > tr:nth-of-type(odd), .table-hover > tbody > tr:hover { + color: unset; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: absolute; + display: block; + width: 8rem; + height: 8rem; + inset: 20vh 0 auto 0; + margin: 0 auto 0 auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:nth-child(2) { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + + .loading-progress foreignObject { + width: 100%; + height: 100%; + } + + .loading-progress foreignObject>img { + width: 50%; + height: 50%; + position: absolute; + object-fit: contain; + object-position: center; + left: calc(25%); + top: calc(25%); + } + + .loading-progress foreignObject>img:nth-of-type(1) { + opacity: calc(100% - var(--blazor-load-percentage, 0%)); + transition: 0.5s; + } + + .loading-progress foreignObject>img:nth-of-type(2) { + opacity: var(--blazor-load-percentage, 0%); + transition: 0.5s; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(30vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/jetbrains-mono.css b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/jetbrains-mono.css new file mode 100644 index 000000000..78aedd2b1 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/jetbrains-mono.css @@ -0,0 +1,118 @@ +/* source: https://gist.github.com/aasmpro/95776294ecf48bd7d0562504bad848ea */ + +/* normal fonts */ + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 100; + src: url("./ttf/JetBrainsMono-Thin.ttf") format("truetype"); + src: url("./webfonts/JetBrainsMono-Thin.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 200; + src: url("./webfonts/JetBrainsMono-ExtraLight.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 300; + src: url("./webfonts/JetBrainsMono-Light.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 400; + src: url("./webfonts/JetBrainsMono-Regular.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 500; + src: url("./webfonts/JetBrainsMono-Medium.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 600; + src: url("./webfonts/JetBrainsMono-SemiBold.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 700; + src: url("./webfonts/JetBrainsMono-Bold.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 800; + src: url("./webfonts/JetBrainsMono-ExtraBold.woff2") format("woff2"); +} + +/* italic fonts */ + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 100; + src: url("./webfonts/JetBrainsMono-ThinItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 200; + src: url("./webfonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 300; + src: url("./webfonts/JetBrainsMono-LightItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 400; + src: url("./webfonts/JetBrainsMono-Italic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 500; + src: url("./webfonts/JetBrainsMono-MediumItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 600; + src: url("./webfonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 700; + src: url("./webfonts/JetBrainsMono-BoldItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 800; + src: url("./webfonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2"); +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 new file mode 100644 index 000000000..4917f4341 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 new file mode 100644 index 000000000..536d3f715 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 new file mode 100644 index 000000000..8f88c5464 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 new file mode 100644 index 000000000..d1478bacc Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 new file mode 100644 index 000000000..b97239f32 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 new file mode 100644 index 000000000..be01aaca5 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 new file mode 100644 index 000000000..d60c270e8 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 new file mode 100644 index 000000000..653849873 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 new file mode 100644 index 000000000..66ca3d2b9 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 new file mode 100644 index 000000000..669d04cdf Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 new file mode 100644 index 000000000..80cfd15e0 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 new file mode 100644 index 000000000..40da42765 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 new file mode 100644 index 000000000..5ead7b0d6 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 new file mode 100644 index 000000000..c5dd294b4 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 new file mode 100644 index 000000000..17270e459 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 new file mode 100644 index 000000000..a6432151c Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/favicon.png b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/favicon.png new file mode 100644 index 000000000..8422b5969 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/favicon.png differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/icon-192.png b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/icon-192.png new file mode 100644 index 000000000..166f56da7 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/icon-192.png differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/icon-512.png b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/icon-512.png new file mode 100644 index 000000000..c2dd4842d Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/icon-512.png differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/img/icon.png b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/img/icon.png new file mode 100644 index 000000000..36b59c61e Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/img/icon.png differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/img/icon_white.png b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/img/icon_white.png new file mode 100644 index 000000000..0058bfa9e Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/img/icon_white.png differ diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/index.html b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/index.html new file mode 100644 index 000000000..fa8a79b8e --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/index.html @@ -0,0 +1,43 @@ + + + + + + + Spacebar.Client + + + + + + + + + + + + + +
+ + + + + + + + +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/manifest.webmanifest b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/manifest.webmanifest new file mode 100644 index 000000000..a30ef2e29 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/manifest.webmanifest @@ -0,0 +1,22 @@ +{ + "name": "Spacebar.Client", + "short_name": "Spacebar.Client", + "id": "./", + "start_url": "./", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#03173d", + "prefer_related_applications": false, + "icons": [ + { + "src": "icon-512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "icon-192.png", + "type": "image/png", + "sizes": "192x192" + } + ] +} diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/service-worker.js b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/service-worker.js new file mode 100644 index 000000000..fe614daee --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/service-worker.js @@ -0,0 +1,4 @@ +// In development, always fetch from the network and do not enable offline support. +// This is because caching would make development more difficult (changes would not +// be reflected on the first load after each change). +self.addEventListener('fetch', () => { }); diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/service-worker.published.js b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/service-worker.published.js new file mode 100644 index 000000000..51a0e5c7a --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/service-worker.published.js @@ -0,0 +1,55 @@ +// Caution! Be sure you understand the caveats before publishing an application with +// offline support. See https://aka.ms/blazor-offline-considerations + +self.importScripts('./service-worker-assets.js'); +self.addEventListener('install', event => event.waitUntil(onInstall(event))); +self.addEventListener('activate', event => event.waitUntil(onActivate(event))); +self.addEventListener('fetch', event => event.respondWith(onFetch(event))); + +const cacheNamePrefix = 'offline-cache-'; +const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; +const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.webmanifest$/ ]; +const offlineAssetsExclude = [ /^service-worker\.js$/ ]; + +// Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'. +const base = "/"; +const baseUrl = new URL(base, self.origin); +const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href); + +async function onInstall(event) { + console.info('Service worker: Install'); + + // Fetch and cache all matching items from the assets manifest + const assetsRequests = self.assetsManifest.assets + .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) + .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) + .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); + await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); +} + +async function onActivate(event) { + console.info('Service worker: Activate'); + + // Delete unused caches + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys + .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) + .map(key => caches.delete(key))); +} + +async function onFetch(event) { + let cachedResponse = null; + if (event.request.method === 'GET') { + // For all navigation requests, try to serve index.html from cache, + // unless that request is for an offline resource. + // If you need some URLs to be server-rendered, edit the following check to exclude those URLs + const shouldServeIndexHtml = event.request.mode === 'navigate' + && !manifestUrlList.some(url => url === event.request.url); + + const request = shouldServeIndexHtml ? 'index.html' : event.request; + const cache = await caches.open(cacheName); + cachedResponse = await cache.match(request); + } + + return cachedResponse || fetch(event.request); +} diff --git a/nix/testVm/configuration.nix b/nix/testVm/configuration.nix index a610e8edf..f732ffd78 100644 --- a/nix/testVm/configuration.nix +++ b/nix/testVm/configuration.nix @@ -59,7 +59,7 @@ in }; offload = { - enable = true; + enable = false; gateway = { enableIdentify = true; enableGuildMembers = true; @@ -77,12 +77,12 @@ in }; cdnCs = { - enable = false; + enable = true; extraConfiguration.ConnectionStrings.Spacebar = csConnectionString; }; uApi = { - enable = true; + enable = false; extraConfiguration.ConnectionStrings.Spacebar = csConnectionString; };