@page "/admin/shell" @page "/admin/shell/{DatabaseStr}" @page "/admin/shell/{DatabaseStr}/{DatabaseInstanceStr}" @page "/admin/shell/{DatabaseStr}/{DatabaseInstanceStr}/{Command}" @attribute [Authorize] @using System.Text.RegularExpressions @using Markdig @using Markdown.ColorCode @using Microsoft.Extensions.Options @using MongoDB.Bson.Serialization @using Rms.Risk.Mango.Components.JForms @using Rms.Service.Bootstrap.Security @inject NavigationManager NavigationManager @inject IUserSession UserSession @inject IJSRuntime JsRuntime @inject IDocumentationService DocService @inject IOptions Settings @inject ICommandListService CommandListService @inject IPasswordManager PasswordManager @* * dbMango * * Copyright 2025 Deutsche Bank AG * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *@

Shell, Command: @CommandName

@if (!string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl)) { }
Documentation Use encrypted sensitive data in form of [*...]. You can use JSON array to run multiple commands.
@if (!string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl)) {
                        @Doc
                    
}
@if (!string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl) && ShowDoc) {
@Description
} else {
                        @Result
                    
}
@code { [CascadingParameter] public IModalService Modal { get; set; } = null!; [Parameter] public string? DatabaseStr { get; set; } [Parameter] public string? DatabaseInstanceStr { get; set; } [Parameter] public string? Command { get; set; } private string Text { get; set { if (field == value) return; field = value; CommandName = GetCommandName(field); if ( string.IsNullOrWhiteSpace(CommandName) || CommandName.Length < 4) { Doc = new(); return; } _ = Task.Run(async () => { await LoadDocumentation(); }); } } = @"{ ""ping"": 1 }"; private string Database { get => UserSession.Database; set { if (UserSession.Database == value) return; UserSession.Database = value; SyncUrl(); } } private string DatabaseInstance { get => UserSession.DatabaseInstance; set { if (UserSession.DatabaseInstance == value) return; UserSession.DatabaseInstance = value; SyncUrl(); } } private string Timeout { get; set; } = "20"; private string FetchSize { get; set; } = "5000"; private bool ShowDoc { get; set; } = true; private MarkupString Result { get; set; } private string ResultStr { get; set; } = ""; private MarkupString Doc { get; set; } private string Shard { get; set; } = "- cluster -"; private bool IsReady { get; set; } private MarkupString Description { get; set; } private EditContext? _editContext; private EditContext EditContext => _editContext!; private bool CanCopyUrl => JsonUtils.IsValidJson(Text); private bool CanCopyResults => !string.IsNullOrWhiteSpace(ResultStr) && !ShowDoc; private readonly MarkdownPipeline _htmlPipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .UseColorCode() .Build() ; // ReSharper disable once NotAccessedPositionalProperty.Global public record ShardProperties(string ReplicaSet, string Host, int Port); private readonly Dictionary _shardConnectionStrings = []; private FormCodeEditor _codeEditor = null!; private IReadOnlyCollection Shards => new [] {"- cluster -", "- admin -", "- config -"} .Concat(_shardConnectionStrings.Keys.OrderBy(x => x)) .ToList(); protected override void OnInitialized() { _editContext = new (this); } protected override void OnAfterRender(bool firstRender) { if (!firstRender) return; ShowDoc = !string.IsNullOrWhiteSpace(Settings.Value.MongoDbDocUrl); if (string.IsNullOrWhiteSpace(DatabaseStr)) DatabaseStr = Database; else Database = DatabaseStr; if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) DatabaseInstanceStr = DatabaseInstance; else DatabaseInstance = DatabaseInstanceStr; if ( !string.IsNullOrWhiteSpace(Command) ) { if ( JsonUtils.IsValidJson(Command) ) { try { Text = JsonUtils.FormatJson(Command); } catch(Exception) { // ignore } } else if ( Regex.IsMatch( Command.Trim(),@"^(\w|[0-9_])*$") ) { Text = $"{{\n \"{Command.Trim()}\" : 1\n}}"; } } SyncUrl(); Task.Run(LoadCommands); Task.Run(LoadShards); IsReady = true; StateHasChanged(); } private async Task LoadCommands() { var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var commands = await CommandListService.GetCommands(UserSession.MongoDbAdminForAdminDatabase, cts.Token); await InvokeAsync(() => UpdateCommands(commands)); } private async Task UpdateCommands(IReadOnlyCollection commands) { await JsRuntime.InvokeVoidAsync("DashboardUtils.SetMongoDbCommands", [commands.Select(x => x.Name).ToArray()] ); StateHasChanged(); } public static async Task> LoadShards(IUserSession userSession) { try { if (!userSession.DatabaseConfig.AllowShardAccess) return []; var listShards = BsonDocument.Parse(@"{ listShards : 1 }"); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var doc = await userSession.MongoDbAdminForAdminDatabase.RunCommand(listShards, cts.Token); var re = new Regex("(?[^/]*)/?(?[^:]+):(?[0-9]+)"); var shards = new Dictionary(); foreach (var el in doc["shards"].AsBsonArray.Select( x => x.AsBsonDocument)) { var s = el["host"].AsString; var m = re.Match(s); if ( !m.Success) continue; shards[el["_id"].AsString] = new( m.Groups["RS"].Value, m.Groups["HOST"].Value, int.Parse(m.Groups["PORT"].Value) ); } return shards; } catch (Exception) { return []; } } private async Task LoadShards() { var shards = await LoadShards(UserSession); _shardConnectionStrings.Clear(); foreach (var shard in shards) { _shardConnectionStrings.Add(shard.Key, shard.Value); } await InvokeAsync(StateHasChanged); } private static readonly Regex _commandRegex = new (@"\s*\{\s*""?(?[a-zA-Z]+)""?\s*\:.*"); private string? CommandName { get; set { if (field == value) return; field = value; InvokeAsync(StateHasChanged); } } private static string? GetCommandName(string text) { var s = text[..Math.Min(text.Length, 30)]; var m = _commandRegex.Match(s); if (!m.Success) return null; var n = m.Groups["cmd"].Value; return string.IsNullOrWhiteSpace(n) ? null : n; } private async Task Run() { var ticket = await CanExecuteCommand(); if (string.IsNullOrWhiteSpace(ticket)) return; var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); if (Text[..Math.Min(100, Text.Length)].Trim().StartsWith("[")) { await RunScript(ticket, cts.Token); } else { await RunSingle(ticket, cts.Token); } } private async Task RunSingle(string ticket, CancellationToken token) { try { IsReady = false; ShowDoc = false; await InvokeAsync(StateHasChanged); var clearText = DecryptPasswords(Text); var doc = BsonDocument.Parse(clearText); UpdateComment(doc, ticket, UserSession.User.GetEmail()); var res = await RunCommand(doc, Text, token); await Display(res); var json = doc.ToJson( new() { Indent = false }); SyncUrl(json); } catch (Exception e) { await Display(e); } finally { IsReady = true; await InvokeAsync(StateHasChanged); } } private async Task RunScript(string ticket, CancellationToken token) { List res = []; try { IsReady = false; ShowDoc = false; await InvokeAsync(StateHasChanged); var clearText = DecryptPasswords(Text); // Parse the Text JSON into a BsonArray var docs = BsonSerializer.Deserialize(clearText).Select(p => p.AsBsonDocument); foreach ( var doc in docs) { UpdateComment(doc, ticket, UserSession.User.GetEmail()); res.Add(doc); res.Add(new() { { "----------------------------------", "----------------------------------" } }); res.AddRange(await RunCommand(doc, Text, token)); res.Add(new() { { "==================================", "==================================" } }); await Display(res); } } catch (Exception e) { await Display(e); } finally { IsReady = true; await InvokeAsync(StateHasChanged); } } private string DecryptPasswords(string text) { if (string.IsNullOrWhiteSpace(text)) return text; var regex = new Regex(@"\[(\*.+)\]"); var matches = regex.Matches(text); foreach (Match match in matches) { if (match.Groups.Count <= 1) continue; var encryptedValue = match.Groups[1].Value; var decryptedValue = PasswordManager.DecryptPassword(encryptedValue); text = text.Replace(match.Value, decryptedValue); } return text; } private async Task Format() { Text = JsonUtils.FormatJson(Text); await InvokeAsync(StateHasChanged); } public static async Task CanExecuteCommand(IUserSession userSession, Func, Task> invokeAsync, IModalService modal) { var check = new [] { false }; await invokeAsync(async () => check[0] = await userSession.HasValidTask()); if (check[0]) return userSession.TaskNumber; var res = await ModalDialogUtils.ShowConfirmationDialogWithInput( modal, "Elevated execution", "Please enter a valid task number number to continue. It needs to be in execution state and has an open time window.", "Valid task number:", userSession.TaskNumber ); if (string.IsNullOrWhiteSpace(res)) return null; userSession.TaskNumber = res; await invokeAsync(async () => check[0] = await userSession.HasValidTask()); if (check[0]) return userSession.TaskNumber; var msg = userSession.TaskCheckError ?? "The given task number is not valid: " + res; await ModalDialogUtils.ShowInfoDialog(modal, "Execution not allowed", msg); return null; } public static void UpdateComment(BsonDocument parsed, string ticket, string email) { // comment field does not always work on database version 4.xx or lower /* var comment = $"ticket: {ticket}, email: {email}"; if (parsed == null) throw new("Can't parse the command Json"); // replace existing parsed["comment"] = comment; */ } private Task CanExecuteCommand() => CanExecuteCommand(UserSession, InvokeAsync, Modal); private Task Display(Exception e) { Result = (MarkupString)e.ToString(); return InvokeAsync(StateHasChanged); } private Task Display(IEnumerable res) { var json = res.ToJson(new() { Indent = true }); ResultStr = json; var md = $"```json\n{json}\n```"; Result = new(Markdown.ToHtml(md, _htmlPipeline)); return InvokeAsync(StateHasChanged); } private async Task> RunCommand(BsonDocument doc, string originalCommand, CancellationToken token) { var service = Shard switch { "- admin -" => UserSession.MongoDbAdminForAdminDatabase, "- cluster -" => UserSession.MongoDbAdmin, "- config -" => UserSession.GetCustomAdmin(Database, "config"), _ => UserSession.GetShardConnection(_shardConnectionStrings[Shard].Host, _shardConnectionStrings[Shard].Port) }; var result = await service.RunCommand(doc, originalCommand, token); return [result]; } private void SyncUrl(string? json = null) { var url = GetUrl(json); JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); } private string GetUrl(string? json) { var url = NavigationManager.BaseUri + $"admin/shell/{Database}"; if (!string.IsNullOrWhiteSpace(DatabaseInstance)) url += $"/{DatabaseInstance}"; if ( json != null ) { url += $"/{Uri.EscapeDataString(json)}"; } else if (CommandName != null) { url += $"/{CommandName}"; } return url; } private async Task LoadDocumentation() { var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var hint = $"```javascript\n{await DocService.TryGetHint(CommandName!, cts.Token) ?? ""}\n```"; Doc = new(Markdown.ToHtml(hint, _htmlPipeline) ); await InvokeAsync(StateHasChanged); var md = await DocService.TryGetMarkdown(CommandName!, cts.Token); if (string.IsNullOrWhiteSpace(md)) { Description = new(); return; } var html = Markdown.ToHtml(md, _htmlPipeline); Description = new(html); await InvokeAsync(StateHasChanged); } private async Task CopyUrl() { try { var url = GetUrl(Text); await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", url); await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", "Url was copied to clipboard."); } catch (Exception ex) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); } } private async Task CopyResults() { try { await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", ResultStr); await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", "Results was copied to clipboard."); } catch ( Exception ex) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); } } /// /// Display a user information dialog box, allows the user to click ok or cancel /// public static async Task ShowEncryptionDialog( IModalService service, string? initialPassword = null ) { var parameters = new ModalParameters { { "ShowCancel", true } }; if (initialPassword != null) parameters.Add("ClearText", initialPassword); var options = new ModalOptions { HideCloseButton = false, DisableBackgroundCancel = false }; var form = service.Show("Encrypt", parameters, options); return await form.Result; } private async Task OnEncrypt() { var res = await ShowEncryptionDialog(Modal); if (!res.Cancelled) { var encrypted = (string?)res.Data; if ( string.IsNullOrWhiteSpace(encrypted) ) return; var toInsert = $"[{encrypted}]"; await _codeEditor.InsertAtCursor(toInsert); await InvokeAsync(StateHasChanged); } } }