diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index 51f85fed4..45f5fd561 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -135,6 +135,7 @@ namespace SharedLibraryCore.Configuration TimeSpan.FromDays(30) }; + [ConfigurationIgnore] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")] public Dictionary PresetPenaltyReasons { get; set; } = new Dictionary {{"afk", "Away from keyboard"}, {"ci", "Connection interrupted. Reconnect"}}; @@ -150,6 +151,7 @@ namespace SharedLibraryCore.Configuration [ConfigurationIgnore] public TimeSpan ServerDataCollectionInterval { get; set; } = TimeSpan.FromMinutes(5); + [ConfigurationIgnore] public Dictionary OverridePermissionLevelNames { get; set; } = Enum .GetValues(typeof(Permission)) .Cast() diff --git a/WebfrontCore/Controllers/ConfigurationController.cs b/WebfrontCore/Controllers/ConfigurationController.cs index 2e6edf489..5eee32445 100644 --- a/WebfrontCore/Controllers/ConfigurationController.cs +++ b/WebfrontCore/Controllers/ConfigurationController.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Authorization; +using System; +using System.IO; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SharedLibraryCore; using SharedLibraryCore.Configuration; @@ -7,7 +9,11 @@ using SharedLibraryCore.Configuration.Validation; using SharedLibraryCore.Interfaces; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using WebfrontCore.ViewModels; namespace WebfrontCore.Controllers @@ -36,6 +42,84 @@ namespace WebfrontCore.Controllers return View("Index", Manager.GetApplicationSettings().Configuration()); } + public async Task Files() + { + if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner) + { + return Unauthorized(); + } + + try + { + // todo: move this into a service a some point + var model = await Task.WhenAll(System.IO.Directory + .GetFiles(System.IO.Path.Join(Utilities.OperatingDirectory, "Configuration")) + .Where(file => file.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase)) + .Select(async fileName => new ConfigurationFileInfo + { + FileName = fileName.Split(System.IO.Path.DirectorySeparatorChar).Last(), + FileContent = await System.IO.File.ReadAllTextAsync(fileName) + })); + + return View(model); + } + catch (Exception ex) + { + return Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); + } + } + + [HttpPatch("{Controller}/File/{fileName}")] + public async Task PatchFiles([FromRoute] string fileName) + { + if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner) + { + return Unauthorized(); + } + + if (!fileName.EndsWith(".json")) + { + return BadRequest("File must be of json format."); + } + + using var reader = new StreamReader(Request.Body, Encoding.UTF8); + var content = await reader.ReadToEndAsync(); + + if (string.IsNullOrEmpty(content)) + { + return BadRequest("File content cannot be empty"); + } + + try + { + var file = JObject.Parse(content); + } + catch (JsonReaderException ex) + { + return BadRequest($"{fileName}: {ex.Message}"); + } + + var path = System.IO.Path.Join(Utilities.OperatingDirectory, "Configuration", + fileName.Replace($"{System.IO.Path.DirectorySeparatorChar}", "")); + + // todo: move into a service at some point + if (!System.IO.File.Exists(path)) + { + return BadRequest($"{fileName} does not exist"); + } + + try + { + await System.IO.File.WriteAllTextAsync(path, content); + } + catch (Exception ex) + { + return Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); + } + + return NoContent(); + } + /// /// Endpoint for the save action /// @@ -58,12 +142,19 @@ namespace WebfrontCore.Controllers var currentConfiguration = Manager.GetApplicationSettings().Configuration(); CopyConfiguration(newConfiguration, currentConfiguration); await Manager.GetApplicationSettings().Save(); - return Ok(new { message = new[] { Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_SAVED"] } }); + return Ok(new + { + message = new[] {Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_SAVED"]} + }); } else { - return BadRequest(new { message = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_SAVE_FAILED"], errors = new[] { validationResult.Errors.Select(_error => _error.ErrorMessage) } }); + return BadRequest(new + { + message = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_SAVE_FAILED"], + errors = new[] {validationResult.Errors.Select(_error => _error.ErrorMessage)} + }); } } @@ -76,7 +167,7 @@ namespace WebfrontCore.Controllers void cleanProperties(object config) { foreach (var property in config.GetType() - .GetProperties().Where(_prop => _prop.CanWrite)) + .GetProperties().Where(_prop => _prop.CanWrite)) { var newPropValue = property.GetValue(config); @@ -106,7 +197,8 @@ namespace WebfrontCore.Controllers /// /// Source config /// Destination config - private void CopyConfiguration(ApplicationConfiguration newConfiguration, ApplicationConfiguration oldConfiguration) + private void CopyConfiguration(ApplicationConfiguration newConfiguration, + ApplicationConfiguration oldConfiguration) { foreach (var property in newConfiguration.GetType() .GetProperties().Where(_prop => _prop.CanWrite)) @@ -161,7 +253,7 @@ namespace WebfrontCore.Controllers /// property info of the current property /// private bool ShouldIgnoreProperty(PropertyInfo info) => (info.GetCustomAttributes(false) - .Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore)) - .FirstOrDefault() as ConfigurationIgnore) != null; + .Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore)) + .FirstOrDefault() as ConfigurationIgnore) != null; } } \ No newline at end of file diff --git a/WebfrontCore/ViewModels/ConfigurationFileInfo.cs b/WebfrontCore/ViewModels/ConfigurationFileInfo.cs new file mode 100644 index 000000000..b4569ef45 --- /dev/null +++ b/WebfrontCore/ViewModels/ConfigurationFileInfo.cs @@ -0,0 +1,8 @@ +namespace WebfrontCore.ViewModels +{ + public class ConfigurationFileInfo + { + public string FileName { get; set; } + public string FileContent { get; set; } + } +} \ No newline at end of file diff --git a/WebfrontCore/Views/Configuration/Files.cshtml b/WebfrontCore/Views/Configuration/Files.cshtml new file mode 100644 index 000000000..a8e76f07d --- /dev/null +++ b/WebfrontCore/Views/Configuration/Files.cshtml @@ -0,0 +1,58 @@ +@model IEnumerable +@{ + ViewData["Title"] = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_TITLE"]; + var noticeText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_SAVING_CHANGES"]; + static string FormatHtmlId(string value) => value?.Replace(".", "").Replace(" ", "_"); +} + +@section styles +{ + + +} + +
+
+

@ViewData["Title"]

+
@noticeText
+ + + +
+
+
+
+ @foreach (var file in Model) + { +
+ + @file.FileName +
+
+
@file.FileContent
+ +
+ } +
+
+ +
+ + @section scripts + { + + + + + + + } +
\ No newline at end of file diff --git a/WebfrontCore/Views/Configuration/Index.cshtml b/WebfrontCore/Views/Configuration/Index.cshtml index 1037aa9c9..98b5fed2a 100644 --- a/WebfrontCore/Views/Configuration/Index.cshtml +++ b/WebfrontCore/Views/Configuration/Index.cshtml @@ -16,19 +16,19 @@ string[] getLinkedPropertyName(System.Reflection.PropertyInfo info) { var test = (info.GetCustomAttributes(false) - .Where(_attr => _attr.GetType() == typeof(ConfigurationLinked)) - .FirstOrDefault() as ConfigurationLinked); + .Where(_attr => _attr.GetType() == typeof(ConfigurationLinked)) + .FirstOrDefault() as ConfigurationLinked); return test?.LinkedPropertyNames ?? new string[0]; } bool shouldIgnore(System.Reflection.PropertyInfo info) => (info.GetCustomAttributes(false) - .Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore)) - .FirstOrDefault() as ConfigurationIgnore) != null; + .Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore)) + .FirstOrDefault() as ConfigurationIgnore) != null; bool isOptional(System.Reflection.PropertyInfo info) => (info.GetCustomAttributes(false) - .Where(_attr => _attr.GetType() == typeof(ConfigurationOptional)) - .FirstOrDefault() as ConfigurationOptional) != null; + .Where(_attr => _attr.GetType() == typeof(ConfigurationOptional)) + .FirstOrDefault() as ConfigurationOptional) != null; bool hasLinkedParent(System.Reflection.PropertyInfo info) { @@ -40,88 +40,104 @@

@ViewData["Title"]

@noticeText
-
- @foreach (var property in properties) - { - if (shouldIgnore(property)) - { - continue; - } - string[] linkedPropertyNames = getLinkedPropertyName(property); + - // bool type - if (property.PropertyType == typeof(bool)) - { -
- @Html.Editor(property.Name, linkedPropertyNames.Length > 0 ? new { htmlAttributes = new { @class = "has-related-content mb-0", data_related_content = string.Join(',', linkedPropertyNames.Select(_id => $"#{_id}_content")) } } : null) - @Html.Label(property.Name, null, new { @class = "form-check-label ml-1" }) -
- } - - // array type - else if (property.PropertyType.IsArray) - { - // special type for server config, I don't like this but for now it's ok - @if (property.PropertyType.GetElementType() == typeof(ServerConfiguration)) +
+
+ + @foreach (var property in properties) { -
- @for (int i = 0; i < Model.Servers.Length; i++) + if (shouldIgnore(property)) + { + continue; + } + + string[] linkedPropertyNames = getLinkedPropertyName(property); + + // bool type + if (property.PropertyType == typeof(bool)) + { +
+ @Html.Editor(property.Name, linkedPropertyNames.Length > 0 ? new {htmlAttributes = new {@class = "has-related-content mb-0", data_related_content = string.Join(',', linkedPropertyNames.Select(_id => $"#{_id}_content"))}} : null) + @Html.Label(property.Name, null, new {@class = "form-check-label ml-1"}) +
+ } + + // array type + else if (property.PropertyType.IsArray) + { + // special type for server config, I don't like this but for now it's ok + @if (property.PropertyType.GetElementType() == typeof(ServerConfiguration)) { - @Html.EditorFor(model => model.Servers[i]); +
+ @for (int i = 0; i < Model.Servers.Length; i++) + { + @Html.EditorFor(model => model.Servers[i]) + ; + } + @addServerText +
} - @addServerText -
- } - else if (hasLinkedParent(property)) - { -
- @if (linkedPropertyNames.Length == 0) + else if (hasLinkedParent(property)) { - @Html.Label(property.Name, null, new { @class = "mt-2 d-block" }) +
+ @if (linkedPropertyNames.Length == 0) + { + @Html.Label(property.Name, null, new {@class = "mt-2 d-block"}) + } + @Html.Editor(property.Name, new {htmlAttributes = new {@class = $"form-group form-control bg-dark text-white-50 {(linkedPropertyNames.Length == 0 ? "mb-3" : "mb-0")}"}}) + @addText +
} - @Html.Editor(property.Name, new { htmlAttributes = new { @class = $"form-group form-control bg-dark text-white-50 {(linkedPropertyNames.Length == 0 ? "mb-3" : "mb-0")}" } }) - @addText -
- } - else - { - @Html.Label(property.Name, null, new { @class = "bg-primary pl-3 pr-3 p-2 mb-0 w-100" }) -
- @Html.Editor(property.Name, new { htmlAttributes = new { @class = "form-control bg-dark text-white-50 mt-3 mb-3", placeholder = isOptional(property) ? optionalText : "" } }) - @addText -
- } - } + else + { + @Html.Label(property.Name, null, new {@class = "bg-primary pl-3 pr-3 p-2 mb-0 w-100"}) +
+ @Html.Editor(property.Name, new {htmlAttributes = new {@class = "form-control bg-dark text-white-50 mt-3 mb-3", placeholder = isOptional(property) ? optionalText : ""}}) + @addText +
+ } + } - else - { - if (hasLinkedParent(property)) - { -
- @Html.Label(property.Name, null, new { @class = "mt-1" }) - @Html.Editor(property.Name, new { htmlAttributes = new { @class = "form-group form-control bg-dark text-white-50 mb-0", placeholder = isOptional(property) ? optionalText : "" } }) -
- } + else + { + if (hasLinkedParent(property)) + { +
+ @Html.Label(property.Name, null, new {@class = "mt-1"}) + @Html.Editor(property.Name, new {htmlAttributes = new {@class = "form-group form-control bg-dark text-white-50 mb-0", placeholder = isOptional(property) ? optionalText : ""}}) +
+ } - else - { - @Html.Label(property.Name, null, new { @class = "bg-primary pl-3 pr-3 p-2 mb-0 w-100" }) -
- @Html.Editor(property.Name, new { htmlAttributes = new { @class = "form-group form-control bg-dark text-white-50 mb-0", placeholder = isOptional(property) ? optionalText : "" } }) -
+ else + { + @Html.Label(property.Name, null, new {@class = "bg-primary pl-3 pr-3 p-2 mb-0 w-100"}) +
+ @Html.Editor(property.Name, new {htmlAttributes = new {@class = "form-group form-control bg-dark text-white-50 mb-0", placeholder = isOptional(property) ? optionalText : ""}}) +
+ } + } } - } - } - - + + +
+
+
+ @section scripts { -} - +} \ No newline at end of file diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index a905b4cce..aea49e916 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -23,6 +23,7 @@ + @await RenderSectionAsync("styles", false)
diff --git a/WebfrontCore/wwwroot/css/src/main.scss b/WebfrontCore/wwwroot/css/src/main.scss index 99b0947fb..2eefedd5e 100644 --- a/WebfrontCore/wwwroot/css/src/main.scss +++ b/WebfrontCore/wwwroot/css/src/main.scss @@ -37,6 +37,10 @@ a.nav-link { border-bottom: 1px solid $primary !important; } +.border-bottom-dark { + border-bottom: 1px solid $body-bg !important; +} + .server-history { background-color: $big-dark; } diff --git a/WebfrontCore/wwwroot/js/configuration.js b/WebfrontCore/wwwroot/js/configuration.js index 3839874f4..e9e818fd8 100644 --- a/WebfrontCore/wwwroot/js/configuration.js +++ b/WebfrontCore/wwwroot/js/configuration.js @@ -73,4 +73,49 @@ return false; }); -}); \ No newline at end of file + + hljs.highlightAll(); + $('.edit-file' ).on('keydown .editable', function(e){ + if(e.keyCode === 9) { + document.execCommand ( 'styleWithCSS', true, null ) + document.execCommand ( 'insertText', true, ' ' ) + e.preventDefault() + } + }); + + $('.expand-file-icon').click((e) => { + const selector = $(e.target).data('editor-id'); + $(selector).toggleClass('d-none').toggleClass('d-flex'); + $(e.target).toggleClass('oi-expand-up', 'oi-expand-down'); + }); + + $('.file-save-button').click(e => { + const id = $(e.target).prev().find('.editable').attr('id'); + const content = document.getElementById(id).textContent; + const file = $(e.target).data('file-name'); + + $.ajax({ + data: content, + type: 'PATCH', + url: 'File/' + file, + contentType: 'text/plain', + complete: function(response) { + if (response.status !== 204) { + $('#actionModal').modal(); + $('#actionModal .modal-message').text(response.responseText); + $('#actionModal .modal-message').addClass('text-danger'); + $('#actionModal .modal-message').fadeIn('fast'); + $(e.target).toggleClass('btn-danger'); + return; + } + + $(e.target).removeClass('btn-danger') + $(e.target).toggleClass('btn-success') + window.setTimeout(function() { + $(e.target).toggleClass('btn-success'); + }, 500); + } + }); + }); +}); +