make the version name match the actual name for FTP deployment

fix rare issue with summing session scores
copy font to expected wwwroot dir in debug mode so we get pretty icons when developing
upgrade some packages

pretty much reworked the entire server web config to support better validation and stuff.. not really a small fix

finish web configuration changes (I think)

finish up configuration changes and update shared library nuget
This commit is contained in:
RaidMax 2020-01-17 17:31:53 -06:00
parent 3a1cfba251
commit 697a752be0
38 changed files with 531 additions and 200 deletions

2
.gitignore vendored
View File

@ -238,3 +238,5 @@ launchSettings.json
/GameLogServer/log_env
**/*.css
/Master/master/persistence
/WebfrontCore/wwwroot/fonts
/WebfrontCore/wwwroot/font

View File

@ -24,11 +24,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
<PackageReference Include="RestEase" Version="1.4.10" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
</ItemGroup>

View File

@ -5,6 +5,7 @@ using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
@ -277,7 +278,7 @@ namespace IW4MAdmin.Application
if (newConfig.Servers == null)
{
ConfigHandler.Set(newConfig);
newConfig.Servers = new List<ServerConfiguration>();
newConfig.Servers = new ServerConfiguration[1];
do
{
@ -292,7 +293,7 @@ namespace IW4MAdmin.Application
serverConfig.AddEventParser(parser);
}
newConfig.Servers.Add((ServerConfiguration)serverConfig.Generate());
newConfig.Servers[0] = (ServerConfiguration)serverConfig.Generate();
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
config = newConfig;
@ -314,6 +315,17 @@ namespace IW4MAdmin.Application
await ConfigHandler.Save();
}
var validator = new ApplicationConfigurationValidator();
var validationResult = validator.Validate(config);
if (!validationResult.IsValid)
{
throw new ConfigurationException(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_CONFIGURATION_ERROR"])
{
Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray()
};
}
foreach (var serverConfig in config.Servers)
{
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
@ -336,7 +348,7 @@ namespace IW4MAdmin.Application
}
}
if (config.Servers.Count == 0)
if (config.Servers.Length == 0)
{
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
}
@ -584,7 +596,7 @@ namespace IW4MAdmin.Application
throw lastException;
}
if (successServers != config.Servers.Count)
if (successServers != config.Servers.Length)
{
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{

View File

@ -2,6 +2,7 @@
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
@ -96,6 +97,15 @@ namespace IW4MAdmin.Application
}
Console.WriteLine(e.Message);
if (e is ConfigurationException cfgE)
{
foreach (string error in cfgE.Errors)
{
Console.WriteLine(error);
}
}
Console.WriteLine(exitMessage);
Console.ReadKey();
return;

View File

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.5" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.5" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

View File

@ -45,7 +45,7 @@ namespace LiveRadar
var parsedEvent = new RadarEvent()
{
Guid = items[0].ConvertGuidToLong(),
Guid = items[0].ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber),
Location = Vector3.Parse(items[1]),
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
Team = items[3],

View File

@ -23,7 +23,7 @@
</Target>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.5" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.5" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -92,7 +92,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
HitLocationCount[hit.HitLoc].Count++;
HitCount++;
if (!isDamage)
{
Kills++;
@ -200,12 +199,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
if (weightedLifetimeAverage > Thresholds.MaxOffset(totalHits) &&
hitLoc.HitCount > 100)
{
//Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
//Log.WriteDebug($"Lifetime Average = {newAverage}");
//Log.WriteDebug($"Bone = {hitLoc.Location}");
//Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
//Log.WriteDebug($"ID = {hit.AttackerId}");
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
@ -437,7 +430,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
ClientId = ClientStats.ClientId,
SessionAngleOffset = AngleDifferenceAverage,
RecoilOffset = hitRecoilAverage,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalMinutes,
CurrentStrain = currentStrain,
CurrentViewAngle = new Vector3(hit.ViewAngles.X, hit.ViewAngles.Y, hit.ViewAngles.Z),
Hits = HitCount,
@ -453,7 +446,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Distance = hit.Distance,
SessionScore = ClientStats.SessionScore,
HitType = hit.DeathType,
SessionSPM = ClientStats.SessionSPM,
SessionSPM = Math.Round(ClientStats.SessionSPM, 0),
StrainAngleBetween = Strain.LastDistance,
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
WeaponId = hit.Weapon,

View File

@ -92,9 +92,13 @@ namespace IW4MAdmin.Plugins.Stats.Models
{
SessionScores[SessionScores.Count - 1] = value;
}
get
{
return SessionScores.Sum();
lock (SessionScores)
{
return SessionScores.Sum();
}
}
}
[NotMapped]

View File

@ -22,7 +22,7 @@ namespace IW4MAdmin.Plugins.Stats
{
public string Name => "Simple Stats";
public float Version => Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "RaidMax";
@ -102,6 +102,11 @@ namespace IW4MAdmin.Plugins.Stats
S.Logger.WriteInfo($"End ScriptKill {scriptKillCount}");
#endif
}
else
{
E.Owner.Logger.WriteDebug("Skipping script kill as it is ignored or data in customcallbacks is outdated/missing");
}
break;
case GameEvent.EventType.Kill:
if (!ShouldIgnoreEvent(E.Origin, E.Target))
@ -149,6 +154,11 @@ namespace IW4MAdmin.Plugins.Stats
S.Logger.WriteInfo($"End ScriptDamage {scriptDamageCount}");
#endif
}
else
{
E.Owner.Logger.WriteDebug("Skipping script damage as it is ignored or data in customcallbacks is outdated/missing");
}
break;
}
}

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.5" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -24,23 +24,18 @@ namespace Tests
var config = new ApplicationConfiguration
{
Servers = new List<ServerConfiguration>()
Servers = new[]
{
new ServerConfiguration()
{
AutoMessages = new List<string>(),
IPAddress = "127.0.0.1",
Password = "test",
Port = 28960,
Rules = new List<string>(),
RConParserVersion = "test",
EventParserVersion = "IW4x (v0.6.0)",
ManualLogPath = logFile
}
},
AutoMessages = new List<string>(),
GlobalRules = new List<string>(),
Maps = new List<MapConfiguration>(),
RConPollRate = int.MaxValue
};

View File

@ -14,7 +14,7 @@
<RunPostBuildEvent>Always</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.5" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.5" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -701,8 +701,8 @@ namespace SharedLibraryCore.Commands
public override Task ExecuteAsync(GameEvent E)
{
if (E.Owner.Manager.GetApplicationSettings().Configuration().GlobalRules?.Count < 1 &&
E.Owner.ServerConfig.Rules?.Count < 1)
if (E.Owner.Manager.GetApplicationSettings().Configuration().GlobalRules?.Length < 1 &&
E.Owner.ServerConfig.Rules?.Length < 1)
{
var _ = E.Message.IsBroadcastCommand() ?
E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RULES_NONE"]) :

View File

@ -1,14 +1,12 @@
using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace SharedLibraryCore.Configuration
{
public class ApplicationConfiguration : IBaseConfiguration
{
[LocalizedDisplayName("SETUP_ENABLE_WEBFRONT")]
[ConfigurationLinked("WebfrontBindUrl", "ManualWebfrontUrl", "WebfrontPrimaryColor", "WebfrontSecondaryColor", "WebfrontCustomBranding")]
public bool EnableWebFront { get; set; }
@ -60,7 +58,7 @@ namespace SharedLibraryCore.Configuration
[ConfigurationLinked("WebfrontConnectionWhitelist")]
public bool EnableWebfrontConnectionWhitelist { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_WHITELIST_LIST")]
public List<string> WebfrontConnectionWhitelist { get; set; }
public string[] WebfrontConnectionWhitelist { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
[ConfigurationLinked("CustomLocale")]
@ -68,7 +66,6 @@ namespace SharedLibraryCore.Configuration
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
public string CustomLocale { get; set; }
[ConfigurationOptional]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_DB_PROVIDER")]
public string DatabaseProvider { get; set; } = "sqlite";
[ConfigurationOptional]
@ -78,26 +75,25 @@ namespace SharedLibraryCore.Configuration
public int RConPollRate { get; set; } = 5000;
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_MAX_TB")]
public TimeSpan MaximumTempBanTime { get; set; } = new TimeSpan(24 * 30, 0, 0);
[LocalizedDisplayName("SETUP_ENABLE_COLOR_CODES")]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_COLOR_CODES")]
public bool EnableColorCodes { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGE_PERIOD")]
public int AutoMessagePeriod { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGES")]
public List<string> AutoMessages { get; set; }
public string[] AutoMessages { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_GLOBAL_RULES")]
public List<string> GlobalRules { get; set; }
public string[] GlobalRules { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_DISALLOWED_NAMES")]
public List<string> DisallowedClientNames { get; set; }
public string[] DisallowedClientNames { get; set; } = new string[0];
[UIHint("ServerConfiguration")]
public List<ServerConfiguration> Servers { get; set; }
public ServerConfiguration[] Servers { get; set; }
[ConfigurationIgnore]
public string Id { get; set; }
[ConfigurationIgnore]
public List<MapConfiguration> Maps { get; set; }
public MapConfiguration[] Maps { get; set; }
[ConfigurationIgnore]
public List<QuickMessageConfiguration> QuickMessages { get; set; }
public QuickMessageConfiguration[] QuickMessages { get; set; }
[ConfigurationIgnore]
public string WebfrontUrl => string.IsNullOrEmpty(ManualWebfrontUrl) ? WebfrontBindUrl?.Replace("0.0.0.0", "127.0.0.1") : ManualWebfrontUrl;

View File

@ -1,17 +1,14 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Configuration
{
public class DefaultConfiguration : IBaseConfiguration
{
public List<string> AutoMessages { get; set; }
public List<string> GlobalRules { get; set; }
public List<MapConfiguration> Maps { get; set; }
public List<QuickMessageConfiguration> QuickMessages {get; set;}
public List<string> DisallowedClientNames { get; set; }
public string[] AutoMessages { get; set; }
public string[] GlobalRules { get; set; }
public MapConfiguration[] Maps { get; set; }
public QuickMessageConfiguration[] QuickMessages {get; set;}
public string[] DisallowedClientNames { get; set; }
public IBaseConfiguration Generate() => this;

View File

@ -15,9 +15,9 @@ namespace SharedLibraryCore.Configuration
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_PASSWORD")]
public string Password { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_RULES")]
public List<string> Rules { get; set; }
public string[] Rules { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_AUTO_MESSAGES")]
public List<string> AutoMessages { get; set; }
public string[] AutoMessages { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_PATH")]
[ConfigurationOptional]
public string ManualLogPath { get; set; }
@ -31,6 +31,9 @@ namespace SharedLibraryCore.Configuration
[ConfigurationOptional]
public Uri GameLogServerUrl { get; set; }
[ConfigurationIgnore]
public int Index { get; set; }
private readonly IList<IRConParser> rconParsers;
private readonly IList<IEventParser> eventParsers;
@ -38,8 +41,8 @@ namespace SharedLibraryCore.Configuration
{
rconParsers = new List<IRConParser>();
eventParsers = new List<IEventParser>();
Rules = new List<string>();
AutoMessages = new List<string>();
Rules = new string[0];
AutoMessages = new string[0];
}
public void AddRConParser(IRConParser parser)
@ -94,8 +97,8 @@ namespace SharedLibraryCore.Configuration
Port = Utilities.PromptInt(loc["SETUP_SERVER_PORT"], null, 1, ushort.MaxValue);
Password = Utilities.PromptString(loc["SETUP_SERVER_RCON"]);
AutoMessages = new List<string>();
Rules = new List<string>();
AutoMessages = new string[0];
Rules = new string[0];
ReservedSlotNumber = loc["SETUP_SERVER_RESERVEDSLOT"].PromptInt(null, 0, 32);
ManualLogPath = null;

View File

@ -0,0 +1,72 @@
using FluentValidation;
using System;
using System.Linq;
namespace SharedLibraryCore.Configuration.Validation
{
/// <summary>
/// Validation class for main application configuration
/// </summary>
public class ApplicationConfigurationValidator : AbstractValidator<ApplicationConfiguration>
{
public ApplicationConfigurationValidator()
{
RuleFor(_app => _app.WebfrontBindUrl)
.NotEmpty();
RuleFor(_app => _app.CustomSayName)
.NotEmpty()
.When(_app => _app.EnableCustomSayName);
RuleFor(_app => _app.SocialLinkAddress)
.NotEmpty()
.When(_app => _app.EnableSocialLink);
RuleFor(_app => _app.SocialLinkTitle)
.NotEmpty()
.When(_app => _app.EnableSocialLink);
RuleFor(_app => _app.CustomParserEncoding)
.NotEmpty()
.When(_app => _app.EnableCustomParserEncoding);
RuleFor(_app => _app.WebfrontConnectionWhitelist)
.NotEmpty()
.When(_app => _app.EnableWebfrontConnectionWhitelist);
RuleForEach(_app => _app.WebfrontConnectionWhitelist)
.Must(_address => System.Net.IPAddress.TryParse(_address, out _));
RuleFor(_app => _app.CustomLocale)
.NotEmpty()
.When(_app => _app.EnableCustomLocale);
RuleFor(_app => _app.DatabaseProvider)
.NotEmpty()
.Must(_provider => new[] { "sqlite", "mysql", "postgresql" }.Contains(_provider));
RuleFor(_app => _app.ConnectionString)
.NotEmpty()
.When(_app => _app.DatabaseProvider != "sqlite");
RuleFor(_app => _app.RConPollRate)
.GreaterThanOrEqualTo(1000);
RuleFor(_app => _app.AutoMessagePeriod)
.GreaterThanOrEqualTo(60);
RuleFor(_app => _app.Servers)
.NotEmpty();
RuleFor(_app => _app.AutoMessages)
.NotNull();
RuleFor(_app => _app.GlobalRules)
.NotNull();
RuleForEach(_app => _app.Servers)
.NotEmpty()
.SetValidator(new ServerConfigurationValidator());
}
}
}

View File

@ -0,0 +1,39 @@
using FluentValidation;
using System.Net;
namespace SharedLibraryCore.Configuration.Validation
{
/// <summary>
/// Validation class for server configuration
/// </summary>
public class ServerConfigurationValidator : AbstractValidator<ServerConfiguration>
{
public ServerConfigurationValidator()
{
RuleFor(_server => _server.IPAddress)
.NotEmpty()
.Must(_address => IPAddress.TryParse(_address, out _));
RuleFor(_server => _server.Port)
.InclusiveBetween(0, ushort.MaxValue);
RuleFor(_server => _server.Password)
.NotEmpty();
RuleForEach(_server => _server.Rules)
.NotEmpty();
RuleForEach(_server => _server.AutoMessages)
.NotEmpty();
RuleFor(_server => _server.RConParserVersion)
.NotEmpty();
RuleFor(_server => _server.EventParserVersion)
.NotEmpty();
RuleFor(_server => _server.ReservedSlotNumber)
.InclusiveBetween(0, 32);
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace SharedLibraryCore.Exceptions
{
public class ConfigurationException : Exception
{
public string[] Errors { get; set; }
public ConfigurationException(string message) : base(message) { }
}
}

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
@ -24,9 +25,12 @@ namespace SharedLibraryCore.Configuration
var configContent = File.ReadAllText(_configurationPath);
_configuration = JsonConvert.DeserializeObject<T>(configContent);
}
catch
catch (Exception e)
{
_configuration = default(T);
throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
{
Errors = new[] { e.Message }
};
}
}

View File

@ -104,6 +104,8 @@ namespace SharedLibraryCore
{
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
})
.CatchClrExceptions());

View File

@ -6,7 +6,7 @@
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2.2.4</Version>
<Version>2.2.5</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -20,8 +20,8 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<AssemblyVersion>2.2.4.0</AssemblyVersion>
<FileVersion>2.2.4.0</FileVersion>
<AssemblyVersion>2.2.5.0</AssemblyVersion>
<FileVersion>2.2.5.0</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -46,21 +46,22 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.6.1" />
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Npgsql" Version="4.1.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />

View File

@ -2,8 +2,11 @@
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using WebfrontCore.ViewModels;
@ -12,11 +15,17 @@ namespace WebfrontCore.Controllers
[Authorize]
public class ConfigurationController : BaseController
{
public ConfigurationController(IManager manager) : base (manager)
{
private readonly ApplicationConfigurationValidator _validator;
public ConfigurationController(IManager manager) : base(manager)
{
_validator = new ApplicationConfigurationValidator();
}
/// <summary>
/// Endpoint to get the current configuration view
/// </summary>
/// <returns></returns>
public IActionResult Edit()
{
if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner)
@ -27,56 +36,132 @@ namespace WebfrontCore.Controllers
return View("Index", Manager.GetApplicationSettings().Configuration());
}
/// <summary>
/// Endpoint for the save action
/// </summary>
/// <param name="newConfiguration">bound configuration</param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Edit(ApplicationConfiguration newConfiguration, bool addNewServer = false, bool shouldSave = false)
public async Task<IActionResult> Save(ApplicationConfiguration newConfiguration)
{
// todo: make this authorization middleware instead of these checks
if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner)
{
return Unauthorized();
}
CleanConfiguration(newConfiguration);
var validationResult = _validator.Validate(newConfiguration);
if (validationResult.IsValid)
{
var currentConfiguration = Manager.GetApplicationSettings().Configuration();
CopyConfiguration(newConfiguration, currentConfiguration);
await Manager.GetApplicationSettings().Save();
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) } });
}
}
/// <summary>
/// Cleans the configuration by removing empty items from from the array
/// </summary>
/// <param name="newConfiguration"></param>
private void CleanConfiguration(ApplicationConfiguration newConfiguration)
{
void cleanProperties(object config)
{
foreach (var property in config.GetType()
.GetProperties().Where(_prop => _prop.CanWrite))
{
var newPropValue = property.GetValue(config);
if (newPropValue is ServerConfiguration[] serverConfig)
{
foreach (var c in serverConfig)
{
cleanProperties(c);
}
}
// this clears out any null or empty items in the string array
if (newPropValue is string[] configArray)
{
newPropValue = configArray.Where(_str => !string.IsNullOrWhiteSpace(_str)).ToArray();
}
property.SetValue(config, newPropValue);
}
}
cleanProperties(newConfiguration);
}
/// <summary>
/// Copies required config fields from new to old
/// </summary>
/// <param name="newConfiguration">Source config</param>
/// <param name="oldConfiguration">Destination config</param>
private void CopyConfiguration(ApplicationConfiguration newConfiguration, ApplicationConfiguration oldConfiguration)
{
foreach (var property in newConfiguration.GetType()
.GetProperties().Where(_prop => _prop.CanWrite))
{
var newPropValue = property.GetValue(newConfiguration);
bool isPropNullArray = property.PropertyType.IsArray && newPropValue == null;
// this prevents us from setting a null array as that could screw reading up
if (!ShouldIgnoreProperty(property) && !isPropNullArray)
{
property.SetValue(oldConfiguration, newPropValue);
}
}
}
/// <summary>
/// Generates the partial view for a new list item
/// </summary>
/// <param name="propertyName">name of the property the input element is generated for</param>
/// <param name="itemCount">how many items exist already</param>
/// <param name="serverIndex">if it's a server property, which one</param>
/// <returns></returns>
public IActionResult GetNewListItem(string propertyName, int itemCount, int serverIndex = -1)
{
if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner)
{
return Unauthorized();
}
if (shouldSave)
// todo: maybe make this cleaner in the future
if (propertyName.StartsWith("Servers") && serverIndex < 0)
{
var currentConfiguration = Manager.GetApplicationSettings().Configuration();
var newConfigurationProperties = newConfiguration.GetType().GetProperties();
foreach (var property in currentConfiguration.GetType().GetProperties())
return PartialView("_ServerItem", new ApplicationConfiguration()
{
var newProp = newConfigurationProperties.First(_prop => _prop.Name == property.Name);
var newPropValue = newProp.GetValue(newConfiguration);
if (newPropValue != null && newProp.CanWrite)
{
property.SetValue(currentConfiguration, newPropValue);
}
}
await Manager.GetApplicationSettings().Save();
Servers = Enumerable.Repeat(new ServerConfiguration(), itemCount + 1).ToArray()
});
}
if (addNewServer)
var model = new BindingHelper()
{
newConfiguration.Servers.Add(new ServerConfiguration());
}
return View("Index", newConfiguration);
}
public IActionResult GetNewListItem(string propertyName, int itemCount)
{
if (Client.Level != SharedLibraryCore.Database.Models.EFClient.Permission.Owner)
{
return Unauthorized();
}
var configInfo = new ConfigurationInfo()
{
NewItemCount = itemCount,
PropertyName = propertyName
Properties = propertyName.Split("."),
ItemIndex = itemCount,
ParentItemIndex = serverIndex
};
return PartialView("_ListItem", configInfo);
return PartialView("_ListItem", model);
}
/// <summary>
/// Indicates if the property should be ignored when cleaning/copying from one config to another
/// </summary>
/// <param name="info">property info of the current property</param>
/// <returns></returns>
private bool ShouldIgnoreProperty(PropertyInfo info) => (info.GetCustomAttributes(false)
.Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore))
.FirstOrDefault() as ConfigurationIgnore) != null;
}
}

View File

@ -1,5 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
@ -13,32 +13,39 @@ namespace WebfrontCore.Middleware
/// </summary>
internal sealed class IPWhitelist
{
private readonly List<byte[]> whitelistedIps;
private readonly RequestDelegate nextRequest;
private readonly byte[][] _whitelistedIps;
private readonly RequestDelegate _nextRequest;
private readonly ILogger _logger;
/// <summary>
/// constructor
/// </summary>
/// <param name="nextRequest"></param>
/// <param name="logger"></param>
/// <param name="whitelistedIps">list of textual ip addresses</param>
public IPWhitelist(RequestDelegate nextRequest, ILogger<IPWhitelist> logger, List<string> whitelistedIps)
public IPWhitelist(RequestDelegate nextRequest, ILogger logger, string[] whitelistedIps)
{
this.whitelistedIps = whitelistedIps.Select(_ip => System.Net.IPAddress.Parse(_ip).GetAddressBytes()).ToList();
this.nextRequest = nextRequest;
_whitelistedIps = whitelistedIps.Select(_ip => System.Net.IPAddress.Parse(_ip).GetAddressBytes()).ToArray();
_nextRequest = nextRequest;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
bool isAlllowed = whitelistedIps.Any(_ip => _ip.SequenceEqual(context.Connection.RemoteIpAddress.GetAddressBytes()));
bool isAlllowed = true;
if (_whitelistedIps.Length > 0)
{
isAlllowed = _whitelistedIps.Any(_ip => _ip.SequenceEqual(context.Connection.RemoteIpAddress.GetAddressBytes()));
}
if (isAlllowed)
{
await nextRequest.Invoke(context);
await _nextRequest.Invoke(context);
}
else
{
_logger.WriteInfo($"Blocking HTTP request from {context.Connection.RemoteIpAddress.ToString()}");
context.Abort();
}
}

View File

@ -96,7 +96,7 @@ namespace WebfrontCore
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, IManager manager)
{
app.UseStatusCodePages(_context =>
{
@ -120,7 +120,7 @@ namespace WebfrontCore
if (Program.Manager.GetApplicationSettings().Configuration().EnableWebfrontConnectionWhitelist)
{
app.UseMiddleware<IPWhitelist>(Program.Manager.GetApplicationSettings().Configuration().WebfrontConnectionWhitelist);
app.UseMiddleware<IPWhitelist>(manager.GetLogger(0), manager.GetApplicationSettings().Configuration().WebfrontConnectionWhitelist);
}
app.UseStaticFiles();

View File

@ -0,0 +1,23 @@
namespace WebfrontCore.ViewModels
{
/// <summary>
/// Helper class that hold information to assist with binding lists of items
/// </summary>
public class BindingHelper
{
/// <summary>
/// Sequential property mapping items
/// </summary>
public string[] Properties { get; set; }
/// <summary>
/// Index in the array this new item lives
/// </summary>
public int ItemIndex { get; set; }
/// <summary>
/// Index in the array of the parent item
/// </summary>
public int ParentItemIndex { get; set; }
}
}

View File

@ -1,19 +0,0 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace WebfrontCore.ViewModels
{
public class ConfigurationInfo
{
public string PropertyName { get; set; }
public PropertyInfo PropertyInfo { get; set; }
public IList PropertyValue { get; set; }
public IBaseConfiguration Configuration { get; set; }
public int NewItemCount { get; set; }
}
}

View File

@ -1,4 +1,5 @@
@using SharedLibraryCore.Configuration.Attributes
@using SharedLibraryCore.Configuration;
@model SharedLibraryCore.Configuration.ApplicationConfiguration
@{
@ -39,7 +40,7 @@
<div class="col-12 text-white-50 ">
<h3 class="text-white">@ViewData["Title"]</h3>
<h5 class="mb-4">@noticeText</h5>
<form method="post">
<form id="configurationForm" asp-controller="Configuration" asp-action="Save" method="post">
@foreach (var property in properties)
{
if (shouldIgnore(property))
@ -49,22 +50,36 @@
string[] linkedPropertyNames = getLinkedPropertyName(property);
if (property.PropertyType.Name == typeof(System.Boolean).Name)
// bool type
if (property.PropertyType == typeof(bool))
{
<div class="form-group form-check bg-primary mb-0 pl-3 pr-3 p-2">
@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.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" })
</div>
}
else if (property.PropertyType.Name.Contains("List"))
// array type
else if (property.PropertyType.IsArray)
{
if (hasLinkedParent(property))
// special type for server config, I don't like this but for now it's ok
@if (property.PropertyType.GetElementType() == typeof(ServerConfiguration))
{
<div id="@($"{property.Name}_content")" class="pl-3 pr-3 pt-2 pb-3 bg-dark">
@for (int i = 0; i < Model.Servers.Length; i++)
{
@Html.EditorFor(model => model.Servers[i]);
}
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="@property.Name" class="btn btn-primary configuration-server-add-new">@addServerText</a>
</div>
}
else if (hasLinkedParent(property))
{
<div id="@($"{property.Name}_content")" class="@(linkedPropertyNames.Length == 0 ? "hide" : "hide") bg-dark pl-3 pr-3 pb-2">
@if (linkedPropertyNames.Length == 0)
{
@Html.Label(property.Name, null, new { @class = "mt-2" })
@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")}" } })
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="@property.Name" class="btn btn-primary configuration-add-new">@addText</a>
@ -76,14 +91,7 @@
@Html.Label(property.Name, null, new { @class = "bg-primary pl-3 pr-3 p-2 mb-0 w-100" })
<div id="@($"{property.Name}_content")" class="pl-3 pr-3 pt-2 pb-3 bg-dark">
@Html.Editor(property.Name, new { htmlAttributes = new { @class = "form-control bg-dark text-white-50 mt-3 mb-3", placeholder = isOptional(property) ? optionalText : "" } })
@if (property.PropertyType.GenericTypeArguments[0].Name == "ServerConfiguration")
{
<button asp-action="Edit" asp-route-addNewServer="true" class="btn btn-primary">@addServerText</button>
}
else
{
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="@property.Name" class="btn btn-primary configuration-add-new">@addText</a>
}
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="@property.Name" class="btn btn-primary configuration-add-new">@addText</a>
</div>
}
}
@ -107,7 +115,7 @@
}
}
}
<button asp-controller="Configuration" asp-action="Edit" asp-route-shouldSave="true" class="btn btn-primary btn-block">@saveText</button>
<button class="btn btn-primary btn-block">@saveText</button>
</form>
</div>
</div>

View File

@ -1,3 +1,13 @@
@model WebfrontCore.ViewModels.ConfigurationInfo
@model WebfrontCore.ViewModels.BindingHelper
<input class="form-control bg-dark text-white-50 mb-2 text-box single-line" id="@($"{Model.PropertyName.Replace('[', '_').Replace(']', '_').Replace('.', '_')}_{Model.NewItemCount - 1}_")" name="@($"{Model.PropertyName}[{Model.NewItemCount- 1}]")" type="text" />
@if (Model.Properties.Length == 1)
{
<!-- we're working off that top level model -->
@Html.Editor($"{Model.Properties.Last()}[{Model.ItemIndex}]", new { htmlAttributes = new { @class = "form-control bg-dark text-white-50 mb-2 text-box single-line" } })
}
@if (Model.Properties.Length > 1)
{
<!-- we're working off child model -->
@Html.Editor($"{Model.Properties.First()}[{Model.ParentItemIndex}].{Model.Properties.Last()}[{Model.ItemIndex}]", new { htmlAttributes = new { @class = "form-control bg-dark text-white-50 mb-2 text-box single-line" } })
}

View File

@ -0,0 +1,10 @@
@model SharedLibraryCore.Configuration.ApplicationConfiguration
@{
int start = Model.Servers.Length - 1;
int end = start + 1;
}
<!-- this is an ugly hack that allows us to create new server layout with correct array indices -->
@for (int i = start; i < end; i++)
{
@Html.EditorFor(model => model.Servers[i]);
}

View File

@ -1,52 +1,59 @@
@model IList<SharedLibraryCore.Configuration.ServerConfiguration>
@model SharedLibraryCore.Configuration.ServerConfiguration
@{
string labelClass = "mb-2 mt-1";
string editorClass = "form-control bg-dark text-white-50 text-box single-line mb-2 mt-0";
string addText = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_ADD"];
string optionalText = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["COMMAND_HELP_OPTIONAL"];
int startAt = ViewBag.AddNew ?? false ? Model.Count - 1 : 0;
int i = 0;
}
@for (int i = startAt; i < Model.Count; i++)
{
<div class="h4 text-white mb-0">@Model[i].IPAddress:@Model[i].Port</div>
<div class="server-configuration-header">
<div class="d-flex h4">
<div class="text-white mb-0">@Model.IPAddress:@Model.Port</div>
<!--span class="oi oi-trash link-inverse ml-auto delete-server-button"></!--span>-->
</div>
<div class="border-bottom mb-3">
@Html.LabelFor(model => model[i].IPAddress, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].IPAddress, new { htmlAttributes = new { @class = editorClass } })
<label asp-for="@Model.IPAddress" class="@labelClass"></label>
<input asp-for="@Model.IPAddress" class="@editorClass" />
@Html.LabelFor(model => model[i].Port, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].Port, new { htmlAttributes = new { @class = editorClass } })
<label asp-for="@Model.Port" class="@labelClass"></label>
<input asp-for="@Model.Port" class="@editorClass" />
@Html.LabelFor(model => model[i].Password, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].Password, new { htmlAttributes = new { @class = editorClass } })
<label asp-for="@Model.Password" class="@labelClass"></label>
<input asp-for="@Model.Password" class="@editorClass" />
@Html.LabelFor(model => model[i].ManualLogPath, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].ManualLogPath, new { htmlAttributes = new { @class = editorClass, placeholder = optionalText } })
<label asp-for="@Model.ManualLogPath" class="@labelClass"></label>
<input asp-for="@Model.ManualLogPath" class="@editorClass" placeholder="@optionalText" />
@Html.LabelFor(model => model[i].GameLogServerUrl, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].GameLogServerUrl, new { htmlAttributes = new { @class = editorClass, placeholder = optionalText } })
<label asp-for="@Model.GameLogServerUrl" class="@labelClass"></label>
<input asp-for="@Model.GameLogServerUrl" class="@editorClass" placeholder="@optionalText" />
@Html.LabelFor(model => model[i].RConParserVersion, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].RConParserVersion, new { htmlAttributes = new { @class = editorClass } })
<label asp-for="@Model.RConParserVersion" class="@labelClass"></label>
<input asp-for="@Model.RConParserVersion" class="@editorClass" />
@Html.LabelFor(model => model[i].EventParserVersion, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].EventParserVersion, new { htmlAttributes = new { @class = editorClass } })
<label asp-for="@Model.EventParserVersion" class="@labelClass"></label>
<input asp-for="@Model.EventParserVersion" class="@editorClass" />
@Html.LabelFor(model => model[i].ReservedSlotNumber, null, new { @class = labelClass })
@Html.EditorFor(model => model[i].ReservedSlotNumber, new { htmlAttributes = new { @class = editorClass } })
<label asp-for="@Model.ReservedSlotNumber" class="@labelClass"></label>
<input asp-for="@Model.ReservedSlotNumber" class="@editorClass" />
<div>
@Html.LabelFor(model => model[i].Rules, null, new { @class = "bg-primary pl-3 pr-3 p-2 w-100 mt-3" })
@Html.EditorFor(model => model[i].Rules, new { htmlAttributes = new { @class = editorClass } })
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="@($"Servers[{i}].Rules")" class="btn btn-primary configuration-add-new mt-2">@addText</a>
<label asp-for="@Model.Rules" class="bg-primary pl-3 pr-3 p-2 w-100 mt-3"></label>
@for(i = 0; i < Model.Rules.Length; i++)
{
<input asp-for="@Model.Rules[i]" class="@editorClass" />
}
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="Servers.Rules" class="btn btn-primary configuration-add-new mt-2">@addText</a>
</div>
<div class="mb-3">
@Html.LabelFor(model => model[i].AutoMessages, null, new { @class = "bg-primary pl-3 pr-3 p-2 w-100 mt-3" })
@Html.EditorFor(model => model[i].AutoMessages, new { htmlAttributes = new { @class = editorClass } })
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="@($"Servers[{i}].Automessages")" class="btn btn-primary configuration-add-new mt-2">@addText</a>
<label asp-for="@Model.AutoMessages" class="bg-primary pl-3 pr-3 p-2 w-100 mt-3"></label>
@for(i = 0; i < Model.AutoMessages.Length; i++)
{
<input asp-for="@Model.AutoMessages[i]" class="@editorClass" />
}
<a asp-controller="Configuration" asp-action="GetNewListItem" asp-route-propertyName="Servers.Automessages" class="btn btn-primary configuration-add-new mt-2">@addText</a>
</div>
</div>
}
@*<button asp-controller="Configuration" asp-action="Edit" asp-route-addNewServer="true" class="btn btn-primary">@addServerText</button>*@
</div>

View File

@ -70,11 +70,7 @@
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(CONFIG)'=='Debug'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
@ -98,6 +94,6 @@
</ProjectExtensions>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="if $(ConfigurationName) == Debug ( &#xD;&#xA;powershell -Command wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o $(ProjectDir)wwwroot\lib\open-iconic\font\css\open-iconic-bootstrap.scss&#xD;&#xA;)" />
<Exec Command="if $(ConfigurationName) == Debug ( &#xD;&#xA;powershell -Command wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o $(ProjectDir)wwwroot\lib\open-iconic\font\css\open-iconic-bootstrap.scss&#xD;&#xA;echo d | xcopy /f /y $(ProjectDir)wwwroot\lib\open-iconic\font\fonts $(ProjectDir)wwwroot\font\&#xD;&#xA;)" />
</Target>
</Project>

View File

@ -1,23 +1,76 @@
$(document).ready(function() {
$.each($('.has-related-content'), function (key, value) {
$.each($('.has-related-content'), function(key, value) {
value = $(value);
if (value.attr('checked') !== undefined && value.attr('checked').length > 0) {
$(value.data('related-content')).slideDown();
}
});
$('input:checkbox').change(function () {
$('input:checkbox').change(function() {
var isChecked = $(this).is(':checked');
isChecked ? $($(this).data('related-content')).slideDown() : $($(this).data('related-content')).slideUp();
});
$('.configuration-add-new').click(function (e) {
// this is used for regular simple form adds
$(document).on('click', '.configuration-add-new', function(e) {
e.preventDefault();
let parentElement = $(this).parent();
$.get($(this).attr('href') + '&itemCount=' + $(this).siblings().length, function (response) {
let parentElement = $(this).parent();
let label = $(this).siblings('label');
let forAttr = $(label).attr('for');
let match = /Servers_+([0-9+])_+.*/g.exec(forAttr);
let additionalData = '';
if (match !== null && match.length === 2) {
additionalData = '&serverIndex=' + match[1].toString();
}
$.get($(this).attr('href') + '&itemCount=' + $(this).siblings('input').length.toString() + additionalData, function (response) {
$(response).insertBefore(parentElement.children().last());
});
});
// this is used for server adds which are little more complex
$(document).on('click', '.configuration-server-add-new', function (e) {
e.preventDefault();
let parentElement = $(this).parent();
$.get($(this).attr('href') + '&itemCount=' + $('.server-configuration-header').length.toString(), function (response) {
$(response).insertBefore(parentElement.children().last());
});
});
// removes the server when clicking the delete button
$(document).on('click', '.delete-server-button', function (e) {
$(this).parents('.server-configuration-header').remove();
});
$('#configurationForm').submit(function (e) {
$.ajax({
data: $(this).serialize(),
type: $(this).attr('method'),
url: $(this).attr('action'),
complete: function(response) {
if (response.status === 200) {
$('#actionModal .modal-message').removeClass('text-danger');
$('#actionModal').data('should-refresh', true);
}
else {
$('#actionModal .modal-message').addClass('text-danger');
}
$('#actionModal .modal-body-content').html('');
let errors = '';
if (response.responseJSON.errors !== undefined) {
errors = response.responseJSON.errors[0].join('<br/>');
}
message = response.responseJSON.message;
$('#actionModal .modal-message').html(message + '<br/>' + errors);
$('#actionModal').modal();
$('#actionModal .modal-message').fadeIn('fast');
}
});
return false;
});
});

View File

@ -85,7 +85,7 @@ steps:
echo changing to encoding for linux start script
dos2unix $(outputFolder)\StartIW4MAdmin.sh
echo creating website version filename
@echo $(Version.Major).$(Version.Minor).$(Version.Build).$(Build.BuildId) > $(Build.ArtifactStagingDirectory)\version_prerelease.txt
@echo IW4MAdmin-$(Version.Major).$(Version.Minor)-$(buildConfiguration)$(Version.Build)b$(Build.BuildId) > $(Build.ArtifactStagingDirectory)\version_prerelease.txt
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
- task: CopyFiles@2