diff --git a/Application/Main.cs b/Application/Main.cs index 32269109a..ff1741487 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -451,6 +451,7 @@ namespace IW4MAdmin.Application .AddSingleton(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) .AddSingleton() .AddTransient() + .AddSingleton() .AddSingleton(translationLookup) .AddDatabaseContextOptions(appConfig); diff --git a/Application/Misc/InteractionRegistration.cs b/Application/Misc/InteractionRegistration.cs new file mode 100644 index 000000000..66d5e14c1 --- /dev/null +++ b/Application/Misc/InteractionRegistration.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Data.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SharedLibraryCore.Interfaces; +using InteractionRegistrationCallback = + System.Func>; + +namespace IW4MAdmin.Application.Misc; + +public class InteractionRegistration : IInteractionRegistration +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly ConcurrentDictionary _interactions = new(); + + public InteractionRegistration(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration) + { + var plugin = _serviceProvider.GetRequiredService>() + .FirstOrDefault(plugin => plugin.Name == source); + + if (plugin is not ScriptPlugin scriptPlugin) + { + return; + } + + var wrappedDelegate = (int? clientId, Reference.Game? game, CancellationToken token) => + Task.FromResult( + scriptPlugin.WrapDelegate(interactionRegistration, clientId, game, token)); + + if (!_interactions.ContainsKey(interactionName)) + { + _interactions.TryAdd(interactionName, wrappedDelegate); + } + else + { + _interactions[interactionName] = wrappedDelegate; + } + } + + public void RegisterInteraction(string interactionName, InteractionRegistrationCallback interactionRegistration) + { + if (!_interactions.ContainsKey(interactionName)) + { + _interactions.TryAdd(interactionName, interactionRegistration); + } + else + { + _interactions[interactionName] = interactionRegistration; + } + } + + public void UnregisterInteraction(string interactionName) + { + if (_interactions.ContainsKey(interactionName)) + { + _interactions.TryRemove(interactionName, out _); + } + } + + public async Task> GetInteractions(int? clientId = null, + Reference.Game? game = null, CancellationToken token = default) + { + return (await Task.WhenAll(_interactions.Select(async kvp => + { + try + { + return await kvp.Value(clientId, game, token); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Could not get interaction for interaction {InteractionName} and ClientId {ClientId}", kvp.Key, + clientId); + return null; + } + }))).Where(interaction => interaction is not null); + } + + public async Task ProcessInteraction(string interactionId, int? clientId = null, + Reference.Game? game = null, CancellationToken token = default) + { + if (!_interactions.ContainsKey(interactionId)) + { + throw new ArgumentException($"Interaction with ID {interactionId} has not been registered"); + } + + try + { + var interaction = await _interactions[interactionId](clientId, game, token); + + if (interaction.Action is not null) + { + return await interaction.Action(clientId, game, token); + } + + if (interaction.ScriptAction is not null) + { + foreach (var plugin in _serviceProvider.GetRequiredService>()) + { + if (plugin is not ScriptPlugin scriptPlugin || scriptPlugin.Name != interaction.Source) + { + continue; + } + + return scriptPlugin.ExecuteAction(interaction.ScriptAction, clientId, game, token); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Could not process interaction for interaction {InteractionName} and ClientId {ClientId}", + interactionId, + clientId); + } + + return null; + } +} diff --git a/Application/Misc/ScriptPlugin.cs b/Application/Misc/ScriptPlugin.cs index a9d18cedc..655e93896 100644 --- a/Application/Misc/ScriptPlugin.cs +++ b/Application/Misc/ScriptPlugin.cs @@ -339,6 +339,41 @@ namespace IW4MAdmin.Application.Misc return Task.CompletedTask; } + public T ExecuteAction(Delegate action, params object[] param) + { + try + { + _onProcessing.Wait(); + var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray(); + var result = action.DynamicInvoke(JsValue.Undefined, args); + return (T)(result as JsValue)?.ToObject(); + } + finally + { + if (_onProcessing.CurrentCount == 0) + { + _onProcessing.Release(1); + } + } + } + + public T WrapDelegate(Delegate act, params object[] args) + { + try + { + _onProcessing.Wait(); + return (T)(act.DynamicInvoke(JsValue.Null, + args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)?.ToObject(); + } + finally + { + if (_onProcessing.CurrentCount == 0) + { + _onProcessing.Release(1); + } + } + } + /// /// finds declared script commands in the script plugin /// diff --git a/Plugins/AutomessageFeed/AutomessageFeed.csproj b/Plugins/AutomessageFeed/AutomessageFeed.csproj index 7f279a2a2..6c0fcce20 100644 --- a/Plugins/AutomessageFeed/AutomessageFeed.csproj +++ b/Plugins/AutomessageFeed/AutomessageFeed.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index 5c9bb5df5..b95df7199 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Login/Login.csproj b/Plugins/Login/Login.csproj index d412e524f..e7b0ff1eb 100644 --- a/Plugins/Login/Login.csproj +++ b/Plugins/Login/Login.csproj @@ -19,7 +19,7 @@ - + diff --git a/Plugins/Mute/Mute.csproj b/Plugins/Mute/Mute.csproj index 0889877e4..570c8bd9a 100644 --- a/Plugins/Mute/Mute.csproj +++ b/Plugins/Mute/Mute.csproj @@ -11,10 +11,10 @@ - + - + diff --git a/Plugins/Mute/Plugin.cs b/Plugins/Mute/Plugin.cs index 8db8e0d14..e8af5a2e2 100644 --- a/Plugins/Mute/Plugin.cs +++ b/Plugins/Mute/Plugin.cs @@ -1,12 +1,18 @@ using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; namespace Mute; public class Plugin : IPlugin { - public Plugin(IMetaServiceV2 metaService) + private readonly IInteractionRegistration _interactionRegistration; + private static readonly string MuteInteraction = nameof(MuteInteraction); + + public Plugin(IMetaServiceV2 metaService, IInteractionRegistration interactionRegistration) { + _interactionRegistration = interactionRegistration; DataManager = new DataManager(metaService); } @@ -45,11 +51,58 @@ public class Plugin : IPlugin public Task OnLoadAsync(IManager manager) { + _interactionRegistration.RegisterInteraction(MuteInteraction, async (clientId, game, token) => + { + if (!clientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game)game.Value)) + { + return null; + } + + var muteState = await DataManager.ReadPersistentData(new EFClient { ClientId = clientId.Value }); + + return muteState is MuteState.Unmuted or MuteState.Unmuting + ? new InteractionData + { + EntityId = clientId, + Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"], + DisplayMeta = "oi-volume-off", + ActionPath = "DynamicAction", + ActionMeta = new() + { + { "InteractionId", "command" }, + { "Data", $"mute @{clientId.Value}" }, + { "ActionButtonLabel", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"] }, + { "Name", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"] }, + { "ShouldRefresh", true.ToString() } + }, + MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator, + Source = Name + } + : new InteractionData + { + EntityId = clientId, + Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"], + DisplayMeta = "oi-volume-high", + ActionPath = "DynamicAction", + ActionMeta = new() + { + { "InteractionId", "command" }, + { "Data", $"mute @{clientId.Value}" }, + { "ActionButtonLabel", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"] }, + { "Name", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"] }, + { "ShouldRefresh", true.ToString() } + }, + MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator, + Source = Name + }; + }); + return Task.CompletedTask; } public Task OnUnloadAsync() { + _interactionRegistration.UnregisterInteraction(MuteInteraction); return Task.CompletedTask; } diff --git a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj index b52745c33..a1afea40b 100644 --- a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj +++ b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/ScriptPlugins/VPNDetection.js b/Plugins/ScriptPlugins/VPNDetection.js index d6c7f605e..c1fae81bf 100644 --- a/Plugins/ScriptPlugins/VPNDetection.js +++ b/Plugins/ScriptPlugins/VPNDetection.js @@ -19,7 +19,7 @@ const commands = [{ const plugin = { author: 'RaidMax', - version: 1.3, + version: 1.4, name: 'VPN Detection Plugin', manager: null, logger: null, @@ -82,9 +82,35 @@ const plugin = { this.configHandler = _configHandler; this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element)); this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`); + + this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration'); + this.interactionRegistration.RegisterScriptInteraction('WhitelistVPN', this.name, (clientId, game, token) => { + if (vpnExceptionIds.includes(clientId)) { + return; + } + + const helpers = importNamespace('SharedLibraryCore.Helpers'); + const interactionData = new helpers.InteractionData(); + + interactionData.EntityId = clientId; + interactionData.Name = 'Whitelist VPN'; + interactionData.DisplayMeta = 'oi-circle-check'; + + interactionData.ActionMeta.Add('InteractionId', 'command'); + interactionData.ActionMeta.Add('Data', `whitelistvpn @${clientId}`); + interactionData.ActionMeta.Add('ActionButtonLabel', 'Allow'); + interactionData.ActionMeta.Add('Name', 'Allow VPN Connection'); + interactionData.ActionMeta.Add('ShouldRefresh', true.toString()); + + interactionData.ActionPath = 'DynamicAction'; + interactionData.MinimumPermission = 3; + interactionData.Source = this.name; + return interactionData; + }); }, onUnloadAsync: function () { + this.interactionRegistration.UnregisterInteraction('WhitelistVPN'); }, onTickAsync: function (server) { diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index b355f5b6e..d7a6c3941 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -17,7 +17,7 @@ - + diff --git a/Plugins/Welcome/Welcome.csproj b/Plugins/Welcome/Welcome.csproj index 30b841bf3..79181edf5 100644 --- a/Plugins/Welcome/Welcome.csproj +++ b/Plugins/Welcome/Welcome.csproj @@ -20,7 +20,7 @@ - + diff --git a/SharedLibraryCore/Dtos/PlayerInfo.cs b/SharedLibraryCore/Dtos/PlayerInfo.cs index 1f8d2008e..bed80e3e4 100644 --- a/SharedLibraryCore/Dtos/PlayerInfo.cs +++ b/SharedLibraryCore/Dtos/PlayerInfo.cs @@ -34,5 +34,6 @@ namespace SharedLibraryCore.Dtos public string CurrentServerName { get; set; } public IGeoLocationResult GeoLocationInfo { get; set; } public ClientNoteMetaResponse NoteMeta { get; set; } + public List Interactions { get; set; } } } diff --git a/SharedLibraryCore/Helpers/InteractionData.cs b/SharedLibraryCore/Helpers/InteractionData.cs new file mode 100644 index 000000000..b29ffb21a --- /dev/null +++ b/SharedLibraryCore/Helpers/InteractionData.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Data.Models.Client; +using SharedLibraryCore.Interfaces; +using InteractionCallback = System.Func>; +using ScriptInteractionCallback = System.Func>; + +namespace SharedLibraryCore.Helpers; + +public class InteractionData : IInteractionData +{ + public int? EntityId { get; set; } + public bool Enabled { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string DisplayMeta { get; set; } + public string ActionPath { get; set; } + public Dictionary ActionMeta { get; set; } = new(); + public string ActionUri => ActionPath + "?" + string.Join('&', ActionMeta.Select(kvp => $"{kvp.Key}={kvp.Value}")); + public EFClient.Permission? MinimumPermission { get; set; } + public string PermissionEntity { get; set; } = "Interaction"; + public string PermissionAccess { get; set; } = "Read"; + public string Source { get; set; } + public InteractionCallback Action { get; set; } + public Delegate ScriptAction { get; set; } +} diff --git a/SharedLibraryCore/Interfaces/IInteractionData.cs b/SharedLibraryCore/Interfaces/IInteractionData.cs new file mode 100644 index 000000000..9bf10bcbb --- /dev/null +++ b/SharedLibraryCore/Interfaces/IInteractionData.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Data.Models.Client; +using InteractionCallback = System.Func>; +using ScriptInteractionCallback = System.Func>; + +namespace SharedLibraryCore.Interfaces; + +public interface IInteractionData +{ + int? EntityId { get; } + bool Enabled { get; } + string Name { get; } + string Description { get; } + string DisplayMeta { get; } + string ActionPath { get; } + Dictionary ActionMeta { get; } + string ActionUri { get; } + EFClient.Permission? MinimumPermission { get; } + string PermissionEntity { get; } + string PermissionAccess { get; } + string Source { get; } + InteractionCallback Action { get; } + Delegate ScriptAction { get; } +} diff --git a/SharedLibraryCore/Interfaces/IInteractionRegistration.cs b/SharedLibraryCore/Interfaces/IInteractionRegistration.cs new file mode 100644 index 000000000..8a941aa50 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IInteractionRegistration.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Data.Models; + +namespace SharedLibraryCore.Interfaces; + +public interface IInteractionRegistration +{ + void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration); + void RegisterInteraction(string interactionName, Func> interactionRegistration); + void UnregisterInteraction(string interactionName); + Task> GetInteractions(int? clientId = null, + Reference.Game? game = null, CancellationToken token = default); + Task ProcessInteraction(string interactionId, int? clientId = null, Reference.Game? game = null, CancellationToken token = default); +} diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 624fb8492..f8bfdee36 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -4,7 +4,7 @@ Library net6.0 RaidMax.IW4MAdmin.SharedLibraryCore - 2022.6.16.1 + 2022.9.8.1 RaidMax Forever None Debug;Release;Prerelease @@ -19,7 +19,7 @@ true MIT Shared Library for IW4MAdmin - 2022.6.16.1 + 2022.9.8.1 true $(NoWarn);1591 diff --git a/WebfrontCore/Controllers/ActionController.cs b/WebfrontCore/Controllers/ActionController.cs index aef08fae4..cb480787e 100644 --- a/WebfrontCore/Controllers/ActionController.cs +++ b/WebfrontCore/Controllers/ActionController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Data.Models; @@ -24,6 +25,7 @@ namespace WebfrontCore.Controllers { private readonly ApplicationConfiguration _appConfig; private readonly IMetaServiceV2 _metaService; + private readonly IInteractionRegistration _interactionRegistration; private readonly string _banCommandName; private readonly string _tempbanCommandName; private readonly string _unbanCommandName; @@ -37,10 +39,12 @@ namespace WebfrontCore.Controllers private readonly string _addClientNoteCommandName; public ActionController(IManager manager, IEnumerable registeredCommands, - ApplicationConfiguration appConfig, IMetaServiceV2 metaService) : base(manager) + ApplicationConfiguration appConfig, IMetaServiceV2 metaService, + IInteractionRegistration interactionRegistration) : base(manager) { _appConfig = appConfig; _metaService = metaService; + _interactionRegistration = interactionRegistration; foreach (var cmd in registeredCommands) { @@ -86,6 +90,81 @@ namespace WebfrontCore.Controllers } } + public IActionResult DynamicActionForm(int? id, string meta) + { + var metaDict = JsonSerializer.Deserialize>(meta); + + if (metaDict is null) + { + return BadRequest(); + } + + metaDict.TryGetValue(nameof(ActionInfo.ActionButtonLabel), out var label); + metaDict.TryGetValue(nameof(ActionInfo.Name), out var name); + metaDict.TryGetValue(nameof(ActionInfo.ShouldRefresh), out var refresh); + metaDict.TryGetValue("Data", out var data); + metaDict.TryGetValue("InteractionId", out var interactionId); + + bool.TryParse(refresh, out var shouldRefresh); + + var inputs = new List + { + new() + { + Name = "InteractionId", + Value = interactionId, + Type = "hidden" + }, + new() + { + Name = "data", + Value = data, + Type = "hidden" + }, + new() + { + Name = "TargetId", + Value = id?.ToString(), + Type = "hidden" + } + }; + + var info = new ActionInfo + { + ActionButtonLabel = label, + Name = name, + Action = nameof(DynamicActionAsync), + ShouldRefresh = shouldRefresh, + Inputs = inputs + }; + + return View("_ActionForm", info); + } + + public async Task DynamicActionAsync(string interactionId, string data, int? targetId, + CancellationToken token = default) + { + if (interactionId == "command") + { + var server = Manager.GetServers().First(); + + return await Task.FromResult(RedirectToAction("Execute", "Console", new + { + serverId = server.EndPoint, + command = $"{_appConfig.CommandPrefix}{data}" + })); + } + + var game = (Reference.Game?)null; + + if (targetId.HasValue) + { + game = (await Manager.GetClientService().Get(targetId.Value))?.GameName; + } + + return Ok(await _interactionRegistration.ProcessInteraction(interactionId, targetId, game, token)); + } + public IActionResult BanForm() { var info = new ActionInfo @@ -292,7 +371,7 @@ namespace WebfrontCore.Controllers { ClientId = Client.ClientId }); - + return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token, $"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}", @@ -645,7 +724,7 @@ namespace WebfrontCore.Controllers $"{_appConfig.CommandPrefix}{_setClientTagCommandName} @{targetId} {clientTag}" })); } - + public async Task AddClientNoteForm(int id) { var existingNote = await _metaService.GetPersistentMetaValue("ClientNotes", id); @@ -682,7 +761,7 @@ namespace WebfrontCore.Controllers } }); } - + var server = Manager.GetServers().First(); return await Task.FromResult(RedirectToAction("Execute", "Console", new { diff --git a/WebfrontCore/Controllers/Client/ClientController.cs b/WebfrontCore/Controllers/Client/ClientController.cs index 1fa7644a0..e08f4164d 100644 --- a/WebfrontCore/Controllers/Client/ClientController.cs +++ b/WebfrontCore/Controllers/Client/ClientController.cs @@ -25,14 +25,16 @@ namespace WebfrontCore.Controllers private readonly StatsConfiguration _config; private readonly IGeoLocationService _geoLocationService; private readonly ClientService _clientService; + private readonly IInteractionRegistration _interactionRegistration; public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config, - IGeoLocationService geoLocationService, ClientService clientService) : base(manager) + IGeoLocationService geoLocationService, ClientService clientService, IInteractionRegistration interactionRegistration) : base(manager) { _metaService = metaService; _config = config; _geoLocationService = geoLocationService; _clientService = clientService; + _interactionRegistration = interactionRegistration; } [Obsolete] @@ -75,6 +77,8 @@ namespace WebfrontCore.Controllers note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId); } + var interactions = await _interactionRegistration.GetInteractions(id, client.GameName, token); + // even though we haven't set their level to "banned" yet // (ie they haven't reconnected with the infringing player identifier) // we want to show them as banned as to not confuse people. @@ -137,7 +141,8 @@ namespace WebfrontCore.Controllers ingameClient.CurrentServer.Port), CurrentServerName = ingameClient?.CurrentServer?.Hostname, GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString), - NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note + NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note, + Interactions = interactions.ToList() }; var meta = await _metaService.GetRuntimeMeta(new ClientPaginationRequest diff --git a/WebfrontCore/Permissions/WebfrontEntity.cs b/WebfrontCore/Permissions/WebfrontEntity.cs index a6a045826..39f615f09 100644 --- a/WebfrontCore/Permissions/WebfrontEntity.cs +++ b/WebfrontCore/Permissions/WebfrontEntity.cs @@ -15,7 +15,8 @@ public enum WebfrontEntity RecentPlayersPage, ProfilePage, AdminMenu, - ClientNote + ClientNote, + Interaction } public enum WebfrontPermission diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index c88fcb556..9fce5a34c 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -105,6 +105,8 @@ namespace WebfrontCore { options.AccessDeniedPath = "/"; options.LoginPath = "/"; + options.Events.OnValidatePrincipal += ClaimsPermissionRemoval.ValidateAsync; + options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn; }); services.AddSingleton(Program.Manager); @@ -138,6 +140,7 @@ namespace WebfrontCore services.AddSingleton(Program.ApplicationServiceProvider .GetRequiredService()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService()); + services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/WebfrontCore/ViewModels/SideContextMenuItem.cs b/WebfrontCore/ViewModels/SideContextMenuItem.cs index 96631d5ee..14823dcd6 100644 --- a/WebfrontCore/ViewModels/SideContextMenuItem.cs +++ b/WebfrontCore/ViewModels/SideContextMenuItem.cs @@ -12,6 +12,7 @@ public class SideContextMenuItem public string Icon { get; set; } public string Tooltip { get; set; } public int? EntityId { get; set; } + public string Meta { get; set; } } diff --git a/WebfrontCore/Views/Action/_ActionForm.cshtml b/WebfrontCore/Views/Action/_ActionForm.cshtml index f8220137a..854c81eec 100644 --- a/WebfrontCore/Views/Action/_ActionForm.cshtml +++ b/WebfrontCore/Views/Action/_ActionForm.cshtml @@ -3,7 +3,7 @@ @{ Layout = null; } - + @if (Model.Inputs.Any(input => input.Type != "hidden")) {
diff --git a/WebfrontCore/Views/Client/Profile/Index.cshtml b/WebfrontCore/Views/Client/Profile/Index.cshtml index 2b0069140..be5f10281 100644 --- a/WebfrontCore/Views/Client/Profile/Index.cshtml +++ b/WebfrontCore/Views/Client/Profile/Index.cshtml @@ -22,7 +22,7 @@ EFPenalty.PenaltyType.Flag => "alert-secondary", EFPenalty.PenaltyType.TempBan => "alert-secondary", _ => "alert" - }; + }; } string ClassForProfileBackground() @@ -391,6 +391,20 @@ }); } + foreach (var interaction in Model.Interactions.Where(i => (int)ViewBag.User.Level >= ((int?)i.MinimumPermission ?? 0))) + { + menuItems.Items.Add(new SideContextMenuItem + { + Title = interaction.Name, + Tooltip = interaction.Description, + EntityId = interaction.EntityId, + Icon = interaction.DisplayMeta, + Reference = interaction.ActionPath, + Meta = System.Text.Json.JsonSerializer.Serialize(interaction.ActionMeta), + IsButton = true + }); + } + } diff --git a/WebfrontCore/Views/Shared/_SideContextMenu.cshtml b/WebfrontCore/Views/Shared/_SideContextMenu.cshtml index 4f853e8c7..caa80faa4 100644 --- a/WebfrontCore/Views/Shared/_SideContextMenu.cshtml +++ b/WebfrontCore/Views/Shared/_SideContextMenu.cshtml @@ -8,7 +8,7 @@ @foreach (var item in Model.Items) { - +