浏览代码

Added google and discord oauth2. Fixed menu bugs

Marcel Baumgartner 2 年之前
父节点
当前提交
60693d25da

+ 132 - 0
Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs

@@ -0,0 +1,132 @@
+using Logging.Net;
+using Microsoft.AspNetCore.Mvc;
+using Moonlight.App.Exceptions;
+using Moonlight.App.Helpers;
+using Moonlight.App.Repositories;
+using Moonlight.App.Services;
+using Moonlight.App.Services.OAuth2;
+using Moonlight.App.Services.Sessions;
+
+namespace Moonlight.App.Http.Controllers.Api.Moonlight;
+
+[ApiController]
+[Route("api/moonlight/oauth2")]
+public class OAuth2Controller : Controller
+{
+    private readonly GoogleOAuth2Service GoogleOAuth2Service;
+    private readonly DiscordOAuth2Service DiscordOAuth2Service;
+    private readonly UserRepository UserRepository;
+    private readonly UserService UserService;
+
+    public OAuth2Controller(
+        GoogleOAuth2Service googleOAuth2Service, 
+        UserRepository userRepository, 
+        UserService userService,
+        DiscordOAuth2Service discordOAuth2Service)
+    {
+        GoogleOAuth2Service = googleOAuth2Service;
+        UserRepository = userRepository;
+        UserService = userService;
+        DiscordOAuth2Service = discordOAuth2Service;
+    }
+
+    [HttpGet("google")]
+    public async Task<ActionResult> Google([FromQuery] string code)
+    {
+        try
+        {
+            var userData = await GoogleOAuth2Service.HandleCode(code);
+
+            if (userData == null)
+                return Redirect("/login");
+
+            try
+            {
+                var user = UserRepository.Get().FirstOrDefault(x => x.Email == userData.Email);
+
+                string token;
+                
+                if (user == null)
+                {
+                    token = await UserService.Register(
+                        userData.Email,
+                        StringHelper.GenerateString(32),
+                        userData.FirstName,
+                        userData.LastName
+                    );
+                }
+                else
+                {
+                    token = await UserService.GenerateToken(user);
+                }
+                
+                Response.Cookies.Append("token", token, new ()
+                {
+                    Expires = new DateTimeOffset(DateTime.UtcNow.AddDays(10))
+                });
+
+                return Redirect("/");
+            }
+            catch (Exception e)
+            {
+                Logger.Warn(e.Message);
+                return Redirect("/login");
+            }
+        }
+        catch (Exception e)
+        {
+            Logger.Warn(e.Message);
+            return BadRequest();
+        }
+    }
+
+    [HttpGet("discord")]
+    public async Task<ActionResult> Discord([FromQuery] string code)
+    {
+        try
+        {
+            var userData = await DiscordOAuth2Service.HandleCode(code);
+
+            if (userData == null)
+                return Redirect("/login");
+
+            try
+            {
+                var user = UserRepository.Get().FirstOrDefault(x => x.Email == userData.Email);
+
+                string token;
+                
+                if (user == null)
+                {
+                    token = await UserService.Register(
+                        userData.Email,
+                        StringHelper.GenerateString(32),
+                        userData.FirstName,
+                        userData.LastName
+                    );
+                }
+                else
+                {
+                    token = await UserService.GenerateToken(user);
+                }
+                
+                Response.Cookies.Append("token", token, new ()
+                {
+                    Expires = new DateTimeOffset(DateTime.UtcNow.AddDays(10))
+                });
+
+                return Redirect("/");
+            }
+            catch (Exception e)
+            {
+                Logger.Warn(e.Message);
+                return Redirect("/login");
+            }
+        }
+        catch (Exception e)
+        {
+            Logger.Warn(e.Message);
+            return BadRequest();
+        }
+    }
+}

+ 21 - 0
Moonlight/App/Models/Google/Requests/GoogleOAuth2CodePayload.cs

@@ -0,0 +1,21 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Google.Requests;
+
+public class GoogleOAuth2CodePayload
+{
+    [JsonProperty("grant_type")]
+    public string GrantType { get; set; } = "authorization_code";
+    
+    [JsonProperty("code")]
+    public string Code { get; set; }
+    
+    [JsonProperty("client_id")]
+    public string ClientId { get; set; }
+    
+    [JsonProperty("client_secret")]
+    public string ClientSecret { get; set; }
+    
+    [JsonProperty("redirect_uri")]
+    public string RedirectUri { get; set; }
+}

+ 127 - 0
Moonlight/App/Services/OAuth2/DiscordOAuth2Service.cs

@@ -0,0 +1,127 @@
+using System.Text;
+using Logging.Net;
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Exceptions;
+using Moonlight.App.Models.Google.Requests;
+using RestSharp;
+
+namespace Moonlight.App.Services.OAuth2;
+
+public class DiscordOAuth2Service
+{
+    private readonly bool Enable;
+    private readonly string ClientId;
+    private readonly string ClientSecret;
+    
+    private readonly bool EnableOverrideUrl;
+    private readonly string OverrideUrl;
+    private readonly string AppUrl;
+
+    public DiscordOAuth2Service(ConfigService configService)
+    {
+        var config = configService
+            .GetSection("Moonlight")
+            .GetSection("OAuth2");
+
+        Enable = config
+            .GetSection("Discord")
+            .GetValue<bool>("Enable");
+
+        if (Enable)
+        {
+            ClientId = config.GetSection("Discord").GetValue<string>("ClientId");
+            ClientSecret = config.GetSection("Discord").GetValue<string>("ClientSecret");
+        }
+
+        EnableOverrideUrl = config.GetValue<bool>("EnableOverrideUrl");
+
+        if (EnableOverrideUrl)
+            OverrideUrl = config.GetValue<string>("OverrideUrl");
+
+        AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
+    }
+    
+    public Task<string> GetUrl()
+    {
+        if (!Enable)
+            throw new DisplayException("Discord OAuth2 not enabled");
+        
+        string url = $"https://discord.com/api/oauth2/authorize?client_id={ClientId}" +
+            $"&redirect_uri={GetBaseUrl()}/api/moonlight/oauth2/discord" +
+            "&response_type=code&scope=identify%20email";
+        
+        return Task.FromResult(
+            url
+        );
+    }
+
+    public async Task<User?> HandleCode(string code)
+    {
+        // Generate access token
+        var endpoint = GetBaseUrl() + "/api/moonlight/oauth2/discord";
+        var discordEndpoint = "https://discordapp.com/api/oauth2/token";
+
+        using var client = new RestClient();
+        var request = new RestRequest(discordEndpoint);
+
+        request.AddParameter("client_id", ClientId);
+        request.AddParameter("client_secret", ClientSecret);
+        request.AddParameter("grant_type", "authorization_code");
+        request.AddParameter("code", code);
+        request.AddParameter("redirect_uri", endpoint);
+        
+        var response = await client.ExecutePostAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            //TODO: Maybe add better error handling
+            Logger.Debug("oAuth2 validate error: " + response.Content!);
+            return null;
+        }
+        
+        // parse response
+        
+        var data = new ConfigurationBuilder().AddJsonStream(
+            new MemoryStream(Encoding.ASCII.GetBytes(response.Content!))
+        ).Build();
+
+        var accessToken = data.GetValue<string>("access_token");
+        
+        // Now, we will call the google api with our access token to get the data we need
+
+        var googlePeopleEndpoint = "https://discordapp.com/api/users/@me";
+        
+        var getRequest = new RestRequest(googlePeopleEndpoint);
+        getRequest.AddHeader("Authorization", $"Bearer {accessToken}");
+
+        var getResponse = await client.ExecuteGetAsync(getRequest);
+        
+        if (!getResponse.IsSuccessful)
+        {
+            //TODO: Maybe add better error handling
+            Logger.Debug("OAuth2 api access error: " + getResponse.Content!);
+            return null;
+        }
+        
+        // Parse response
+        
+        var getData = new ConfigurationBuilder().AddJsonStream(
+            new MemoryStream(Encoding.ASCII.GetBytes(getResponse.Content!))
+        ).Build();
+
+        return new User()
+        {
+            Email = getData.GetValue<string>("email"),
+            FirstName = getData.GetValue<string>("username"),
+            LastName = getData.GetValue<string>("discriminator")
+        };
+    }
+
+    private string GetBaseUrl()
+    {
+        if (EnableOverrideUrl)
+            return OverrideUrl;
+
+        return AppUrl;
+    }
+}

+ 149 - 0
Moonlight/App/Services/OAuth2/GoogleOAuth2Service.cs

@@ -0,0 +1,149 @@
+using System.Text;
+using Logging.Net;
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Exceptions;
+using Moonlight.App.Models.Google.Requests;
+using RestSharp;
+
+namespace Moonlight.App.Services.OAuth2;
+
+public class GoogleOAuth2Service
+{
+    private readonly bool EnableGoogle;
+    private readonly string GoogleClientId;
+    private readonly string GoogleClientSecret;
+    
+    private readonly bool EnableOverrideUrl;
+    private readonly string OverrideUrl;
+    private readonly string AppUrl;
+
+    public GoogleOAuth2Service(ConfigService configService)
+    {
+        var config = configService
+            .GetSection("Moonlight")
+            .GetSection("OAuth2");
+
+        EnableGoogle = config
+            .GetSection("Google")
+            .GetValue<bool>("Enable");
+
+        if (EnableGoogle)
+        {
+            GoogleClientId = config.GetSection("Google").GetValue<string>("ClientId");
+            GoogleClientSecret = config.GetSection("Google").GetValue<string>("ClientSecret");
+        }
+
+        EnableOverrideUrl = config.GetValue<bool>("EnableOverrideUrl");
+
+        if (EnableOverrideUrl)
+            OverrideUrl = config.GetValue<string>("OverrideUrl");
+
+        AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
+    }
+    
+    public Task<string> GetUrl()
+    {
+        if (!EnableGoogle)
+            throw new DisplayException("Google OAuth2 not enabled");
+
+        var endpoint = GetBaseUrl() + "/api/moonlight/oauth2/google";
+        var scope = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email";
+
+        return Task.FromResult(
+            $"https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={GoogleClientId}&redirect_uri={endpoint}&scope={scope}"
+        );
+    }
+
+    public async Task<User?> HandleCode(string code)
+    {
+        // Generate access token
+        var endpoint = GetBaseUrl() + "/api/moonlight/oauth2/google";
+        var googleEndpoint = "https://oauth2.googleapis.com/token";
+        
+        // Setup payload
+        var payload = new GoogleOAuth2CodePayload()
+        {
+            Code = code,
+            RedirectUri = endpoint,
+            ClientId = GoogleClientId,
+            ClientSecret = GoogleClientSecret
+        };
+
+        using var client = new RestClient();
+        var request = new RestRequest(googleEndpoint);
+
+        request.AddBody(payload);
+        var response = await client.ExecutePostAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            //TODO: Maybe add better error handling
+            Logger.Debug("oAuth2 validate error: " + response.Content!);
+            return null;
+        }
+        
+        // parse response
+        
+        var data = new ConfigurationBuilder().AddJsonStream(
+            new MemoryStream(Encoding.ASCII.GetBytes(response.Content!))
+        ).Build();
+
+        var accessToken = data.GetValue<string>("access_token");
+        
+        // Now, we will call the google api with our access token to get the data we need
+
+        var googlePeopleEndpoint = "https://people.googleapis.com/v1/people/me";
+        
+        var getRequest = new RestRequest(googlePeopleEndpoint);
+        getRequest.AddHeader("Authorization", $"Bearer {accessToken}");
+        getRequest.AddParameter("personFields", "names,emailAddresses");
+
+        var getResponse = await client.ExecuteGetAsync(getRequest);
+        
+        if (!getResponse.IsSuccessful)
+        {
+            //TODO: Maybe add better error handling
+            Logger.Debug("OAuth2 api access error: " + getResponse.Content!);
+            return null;
+        }
+        
+        // Parse response
+        
+        var getData = new ConfigurationBuilder().AddJsonStream(
+            new MemoryStream(Encoding.ASCII.GetBytes(getResponse.Content!))
+        ).Build();
+
+        var firstName = getData
+            .GetSection("names")
+            .GetChildren()
+            .First()
+            .GetValue<string>("givenName");
+        
+        var lastName = getData
+            .GetSection("names")
+            .GetChildren()
+            .First()
+            .GetValue<string>("familyName");
+        
+        var email = getData
+            .GetSection("emailAddresses")
+            .GetChildren()
+            .First()
+            .GetValue<string>("value");
+
+        return new()
+        {
+            Email = email,
+            FirstName = firstName,
+            LastName = lastName
+        };
+    }
+
+    private string GetBaseUrl()
+    {
+        if (EnableOverrideUrl)
+            return OverrideUrl;
+
+        return AppUrl;
+    }
+}

+ 19 - 22
Moonlight/App/Services/UserService.cs

@@ -68,13 +68,7 @@ public class UserService
         //var mail = new WelcomeMail(user);
         //await MailService.Send(mail, user);
 
-        return JwtBuilder.Create()
-            .WithAlgorithm(new HMACSHA256Algorithm())
-            .WithSecret(JwtSecret)
-            .AddClaim("exp", DateTimeOffset.UtcNow.AddDays(10).ToUnixTimeSeconds())
-            .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds())
-            .AddClaim("userid", user.Id)
-            .Encode();
+        return await GenerateToken(user);
     }
 
     public Task<bool> CheckTotp(string email, string password)
@@ -123,13 +117,7 @@ public class UserService
             {
                 //AuditLogService.Log("login:success", $"{user.Email} has successfully logged in");
 
-                return JwtBuilder.Create()
-                    .WithAlgorithm(new HMACSHA256Algorithm())
-                    .WithSecret(JwtSecret)
-                    .AddClaim("exp", DateTimeOffset.UtcNow.AddDays(10).ToUnixTimeSeconds())
-                    .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds())
-                    .AddClaim("userid", user.Id)
-                    .Encode();
+                return await GenerateToken(user);
             }
             else
             {
@@ -141,17 +129,11 @@ public class UserService
         {
             //AuditLogService.Log("login:success", $"{user.Email} has successfully logged in");
 
-            return JwtBuilder.Create()
-                .WithAlgorithm(new HMACSHA256Algorithm())
-                .WithSecret(JwtSecret)
-                .AddClaim("exp", DateTimeOffset.UtcNow.AddDays(10).ToUnixTimeSeconds())
-                .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds())
-                .AddClaim("userid", user.Id)
-                .Encode();
+            return await GenerateToken(user!);
         }
     }
 
-    public async Task ChangePassword(User user, string password)
+    public Task ChangePassword(User user, string password)
     {
         user.Password = BCrypt.Net.BCrypt.HashPassword(password);
         user.TokenValidTime = DateTime.Now;
@@ -161,6 +143,8 @@ public class UserService
         //await MailService.Send(mail, user);
             
         //AuditLogService.Log("password:change", "The password has been set to a new one");
+        
+        return Task.CompletedTask;
     }
 
     public Task<User> SftpLogin(int id, string password)
@@ -179,4 +163,17 @@ public class UserService
         //TODO: Log
         throw new Exception("Invalid userid or password");
     }
+
+    public Task<string> GenerateToken(User user)
+    {
+        var token = JwtBuilder.Create()
+            .WithAlgorithm(new HMACSHA256Algorithm())
+            .WithSecret(JwtSecret)
+            .AddClaim("exp", DateTimeOffset.UtcNow.AddDays(10).ToUnixTimeSeconds())
+            .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds())
+            .AddClaim("userid", user.Id)
+            .Encode();
+
+        return Task.FromResult(token);
+    }
 }

+ 1 - 0
Moonlight/Moonlight.csproj

@@ -59,6 +59,7 @@
   <ItemGroup>
     <Folder Include="App\Http\Middleware" />
     <Folder Include="App\Models\AuditLogData" />
+    <Folder Include="App\Models\Google\Resources" />
     <Folder Include="resources\lang" />
     <Folder Include="wwwroot\assets\media" />
   </ItemGroup>

+ 3 - 0
Moonlight/Program.cs

@@ -7,6 +7,7 @@ using Moonlight.App.Repositories;
 using Moonlight.App.Repositories.Servers;
 using Moonlight.App.Services;
 using Moonlight.App.Services.Interop;
+using Moonlight.App.Services.OAuth2;
 using Moonlight.App.Services.Sessions;
 using Moonlight.App.Services.Support;
 
@@ -56,6 +57,8 @@ namespace Moonlight
             builder.Services.AddSingleton<PaperService>();
             builder.Services.AddScoped<ClipboardService>();
             builder.Services.AddSingleton<ResourceService>();
+            builder.Services.AddScoped<GoogleOAuth2Service>();
+            builder.Services.AddScoped<DiscordOAuth2Service>();
 
             builder.Services.AddScoped<AuditLogService>();
             builder.Services.AddScoped<SystemAuditLogService>();

+ 19 - 4
Moonlight/Shared/Components/Auth/Login.razor

@@ -9,6 +9,7 @@
 @using Moonlight.App.Services
 @using Moonlight.App.Exceptions
 @using Logging.Net
+@using Moonlight.App.Services.OAuth2
 @using Moonlight.App.Services.Sessions
 
 @inject AlertService AlertService
@@ -16,6 +17,8 @@
 @inject SmartTranslateService SmartTranslateService
 @inject CookieService CookieService
 @inject NavigationManager NavigationManager
+@inject GoogleOAuth2Service GoogleOAuth2Service
+@inject DiscordOAuth2Service DiscordOAuth2Service
 
 <div class="d-flex flex-center">
     <div class="card rounded-3 w-md-550px">
@@ -35,17 +38,17 @@
 
                         <div class="row g-3 mb-9">
                             <div class="col-md-6">
-                                <a href="#" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
+                                <a href="#" @onclick:preventDefault @onclick="DoDiscord" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
                                     <div class="h-15px me-3">
-                                        <i class="bx bx-md bxl-discord-alt"></i>
+                                        <i class="mb-1 bx bx-md bxl-discord-alt"></i>
                                     </div>
                                     <TL>Sign in with Discord</TL>
                                 </a>
                             </div>
                             <div class="col-md-6">
-                                <a href="#" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
+                                <a href="#" @onclick:preventDefault @onclick="DoGoogle" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
                                     <div class="h-15px me-3">
-                                        <i class="bx bx-md bxl-google"></i>
+                                        <i class="mb-1 bx bx-md bxl-google"></i>
                                     </div>
                                     <TL>Sign in with Google</TL>
                                 </a>
@@ -158,4 +161,16 @@
             Logger.Error(e);
         }
     }
+
+    private async Task DoGoogle()
+    {
+        var url = await GoogleOAuth2Service.GetUrl();
+        NavigationManager.NavigateTo(url, true);
+    }
+    
+    private async Task DoDiscord()
+    {
+        var url = await DiscordOAuth2Service.GetUrl();
+        NavigationManager.NavigateTo(url, true);
+    }
 }

+ 24 - 5
Moonlight/Shared/Components/Auth/Register.razor

@@ -6,8 +6,12 @@
 *@
 
 @using Moonlight.App.Services
+@using Moonlight.App.Services.OAuth2
 
 @inject SmartTranslateService SmartTranslateService
+@inject GoogleOAuth2Service GoogleOAuth2Service
+@inject NavigationManager NavigationManager
+@inject DiscordOAuth2Service DiscordOAuth2Service
 
 <div class="d-flex flex-center">
     <div class="card rounded-3 w-md-550px">
@@ -25,17 +29,17 @@
 
                     <div class="row g-3 mb-9">
                         <div class="col-md-6">
-                            <a href="#" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
+                            <a href="#" @onclick:preventDefault @onclick="DoDiscord" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
                                 <div class="h-15px me-3">
-                                    <i class="bx bx-md bxl-discord-alt"></i>
+                                    <i class="mb-1 bx bx-md bxl-discord-alt"></i>
                                 </div>
                                 <TL>Sign up with Discord</TL>
                             </a>
                         </div>
                         <div class="col-md-6">
-                            <a href="#" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
+                            <a href="#" @onclick:preventDefault @onclick="DoGoogle" class="btn btn-flex btn-outline btn-text-gray-700 btn-active-color-primary bg-state-light flex-center text-nowrap w-100">
                                 <div class="h-15px me-3">
-                                    <i class="bx bx-md bxl-google"></i>
+                                    <i class="mb-1 bx bx-md bxl-google"></i>
                                 </div>
                                 <TL>Sign up with Google</TL>
                             </a>
@@ -85,4 +89,19 @@
             </div>
         </div>
     </div>
-</div>
+</div>
+
+@code
+{
+    private async Task DoGoogle()
+    {
+        var url = await GoogleOAuth2Service.GetUrl();
+        NavigationManager.NavigateTo(url, true);
+    }
+    
+    private async Task DoDiscord()
+    {
+        var url = await DiscordOAuth2Service.GetUrl();
+        NavigationManager.NavigateTo(url, true);
+    }
+}

+ 19 - 0
Moonlight/Shared/Components/ErrorBoundaries/PageErrorBoundary.razor

@@ -1,10 +1,14 @@
 @using Logging.Net
+@using Moonlight.App.Exceptions
 @using Moonlight.App.Services
+@using Moonlight.App.Services.Interop
 @using Moonlight.App.Services.Sessions
 
 @inherits ErrorBoundary
 
 @inject IdentityService IdentityService
+@inject AlertService AlertService
+@inject SmartTranslateService SmartTranslateService
 
 @if (CurrentException is null)
 {
@@ -60,6 +64,21 @@ else
         Logger.Error(exception);
 
         await base.OnErrorAsync(exception);
+
+        if (exception is DisplayException displayException)
+        {
+            Task.Run(async () =>
+            {
+                await AlertService.Error(
+                    SmartTranslateService.Translate("Error"),
+                    SmartTranslateService.Translate(displayException.Message)
+                    );
+            });
+            
+            Recover();
+
+            await InvokeAsync(StateHasChanged);
+        }
     }
 
     public new void Recover()

+ 32 - 16
Moonlight/Shared/Layouts/MainLayout.razor

@@ -8,6 +8,7 @@
 @using Moonlight.App.Services
 @using Moonlight.App.Services.Interop
 @using Moonlight.App.Services.Sessions
+@using Logging.Net
 
 @layout ThemeInit
 @implements IDisposable
@@ -31,7 +32,7 @@
         {
             if (!string.IsNullOrEmpty(pathPart))
             {
-                if(pathPart == pathParts.Last())
+                if (pathPart == pathParts.Last())
                     title += $"{pathPart.FirstCharToUpper()} ";
                 else
                     title += $"{pathPart.FirstCharToUpper()} - ";
@@ -128,11 +129,6 @@
         AddBodyAttribute("data-kt-app-toolbar-enabled", "true");
 
         AddBodyClass("app-default");
-
-        JsRuntime.InvokeVoidAsync("KTModalUpgradePlan.init");
-        JsRuntime.InvokeVoidAsync("KTCreateApp.init");
-        JsRuntime.InvokeVoidAsync("KTModalUserSearch.init");
-        JsRuntime.InvokeVoidAsync("KTModalNewTarget.init");
     }
 
     protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -142,9 +138,7 @@
             try
             {
                 User = await IdentityService.Get();
-
                 await InvokeAsync(StateHasChanged);
-                await Task.Delay(300);
 
                 await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-reset-transition");
                 await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-page-loading");
@@ -157,19 +151,24 @@
                 NavigationManager.LocationChanged += (sender, args) => { SessionService.Refresh(); };
 
                 MessageService.Subscribe<MainLayout, SupportMessage>(
-                    $"support.{User.Id}.message", 
-                    this, 
+                    $"support.{User.Id}.message",
+                    this,
                     async message =>
-                {
-                    if (!NavigationManager.Uri.EndsWith("/support") && (message.IsSupport || message.IsSystem))
                     {
-                        await ToastService.Info($"Support: {message.Message}");
-                    }
-                });
+                        if (!NavigationManager.Uri.EndsWith("/support") && (message.IsSupport || message.IsSystem))
+                        {
+                            await ToastService.Info($"Support: {message.Message}");
+                        }
+                    });
+                
+                RunDelayedMenu(0);
+                RunDelayedMenu(1);
+                RunDelayedMenu(3);
+                RunDelayedMenu(5);
             }
             catch (Exception)
             {
-                // ignored
+    // ignored
             }
         }
     }
@@ -193,4 +192,21 @@
     {
         JsRuntime.InvokeVoidAsync("document.body.classList.add", className);
     }
+
+    private void RunDelayedMenu(int seconds)
+    {
+        Task.Run(async () =>
+        {
+            try
+            {
+                await Task.Delay(TimeSpan.FromSeconds(seconds));
+                await JsRuntime.InvokeVoidAsync("KTMenu.initHandlers");
+            }
+            catch (Exception e)
+            {
+                Logger.Warn("Delayed menu error");
+                Logger.Warn(e);
+            }
+        });
+    }
 }

+ 9 - 0
Moonlight/wwwroot/assets/js/scripts.bundle.js

@@ -2462,8 +2462,17 @@ KTMenu.updateDropdowns = function() {
     }
 }
 
+// Bug fix for menu load initializing
+KTMenu.hasInit = false;
+
 // Global handlers
 KTMenu.initHandlers = function() {
+    
+    if(KTMenu.hasInit)
+        return;
+    
+    KTMenu.hasInit = true;
+    
     // Dropdown handler
     document.addEventListener("click", function(e) {
         var items = document.querySelectorAll('.show.menu-dropdown[data-kt-menu-trigger]');