fix damage event not including log line
complete initiall implementation for "2FA" issue #52 issue #66
This commit is contained in:
parent
2bbf2988da
commit
40f1697c97
@ -10,7 +10,7 @@
|
|||||||
<Authors>RaidMax</Authors>
|
<Authors>RaidMax</Authors>
|
||||||
<Company>Forever None</Company>
|
<Company>Forever None</Company>
|
||||||
<Product>IW4MAdmin</Product>
|
<Product>IW4MAdmin</Product>
|
||||||
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server</Description>
|
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
|
||||||
<Copyright>2019</Copyright>
|
<Copyright>2019</Copyright>
|
||||||
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
|
||||||
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
|
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
|
||||||
|
@ -376,7 +376,7 @@ namespace IW4MAdmin.Application
|
|||||||
Commands.Add(new CPing());
|
Commands.Add(new CPing());
|
||||||
Commands.Add(new CSetGravatar());
|
Commands.Add(new CSetGravatar());
|
||||||
Commands.Add(new CNextMap());
|
Commands.Add(new CNextMap());
|
||||||
Commands.Add(new GenerateTokenCommand());
|
Commands.Add(new RequestTokenCommand());
|
||||||
|
|
||||||
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
||||||
{
|
{
|
||||||
|
@ -87,7 +87,7 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.JoinTeam,
|
Type = GameEvent.EventType.JoinTeam,
|
||||||
Data = eventType,
|
Data = logLine,
|
||||||
Origin = origin,
|
Origin = origin,
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
@ -203,7 +203,7 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Damage,
|
Type = GameEvent.EventType.Damage,
|
||||||
Data = eventType,
|
Data = logLine,
|
||||||
Origin = origin,
|
Origin = origin,
|
||||||
Target = target,
|
Target = target,
|
||||||
Owner = server
|
Owner = server
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Helpers;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@ -10,16 +11,9 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<long, TokenState> _tokens;
|
private readonly ConcurrentDictionary<long, TokenState> _tokens;
|
||||||
private readonly RNGCryptoServiceProvider _random;
|
private readonly RNGCryptoServiceProvider _random;
|
||||||
private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 30);
|
private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 120);
|
||||||
private const short TOKEN_LENGTH = 4;
|
private const short TOKEN_LENGTH = 4;
|
||||||
|
|
||||||
private class TokenState
|
|
||||||
{
|
|
||||||
public long NetworkId { get; set; }
|
|
||||||
public DateTime RequestTime { get; set; } = DateTime.Now;
|
|
||||||
public string Token { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public TokenAuthentication()
|
public TokenAuthentication()
|
||||||
{
|
{
|
||||||
_tokens = new ConcurrentDictionary<long, TokenState>();
|
_tokens = new ConcurrentDictionary<long, TokenState>();
|
||||||
@ -38,32 +32,44 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
return authorizeSuccessful;
|
return authorizeSuccessful;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GenerateNextToken(long networkId)
|
public TokenState GenerateNextToken(long networkId)
|
||||||
{
|
{
|
||||||
TokenState state = null;
|
TokenState state = null;
|
||||||
|
|
||||||
if (_tokens.ContainsKey(networkId))
|
if (_tokens.ContainsKey(networkId))
|
||||||
{
|
{
|
||||||
state = _tokens[networkId];
|
state = _tokens[networkId];
|
||||||
|
|
||||||
if ((DateTime.Now - state.RequestTime) < _timeoutPeriod)
|
if ((DateTime.Now - state.RequestTime) > _timeoutPeriod)
|
||||||
{
|
{
|
||||||
return null;
|
_tokens.TryRemove(networkId, out TokenState _);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_tokens.TryRemove(networkId, out TokenState _);
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state = new TokenState()
|
state = new TokenState()
|
||||||
{
|
{
|
||||||
NetworkId = networkId,
|
NetworkId = networkId,
|
||||||
Token = _generateToken()
|
Token = _generateToken(),
|
||||||
|
TokenDuration = _timeoutPeriod
|
||||||
};
|
};
|
||||||
|
|
||||||
_tokens.TryAdd(networkId, state);
|
_tokens.TryAdd(networkId, state);
|
||||||
return state.Token;
|
|
||||||
|
// perform some housekeeping so we don't have built up tokens if they're not ever used
|
||||||
|
foreach (var (key, value) in _tokens)
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - value.RequestTime) > _timeoutPeriod)
|
||||||
|
{
|
||||||
|
_tokens.TryRemove(key, out TokenState _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string _generateToken()
|
public string _generateToken()
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Login.Commands
|
namespace IW4MAdmin.Plugins.Login.Commands
|
||||||
@ -22,18 +19,22 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
|||||||
public override async Task ExecuteAsync(GameEvent E)
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
{
|
||||||
var client = E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId];
|
var client = E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId];
|
||||||
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, client.PasswordSalt));
|
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data);
|
||||||
|
|
||||||
if (hashedPassword[0] == client.Password)
|
if (!success)
|
||||||
{
|
{
|
||||||
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, client.PasswordSalt));
|
||||||
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]);
|
|
||||||
|
if (hashedPassword[0] == client.Password)
|
||||||
|
{
|
||||||
|
success = true;
|
||||||
|
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
_ = success ?
|
||||||
{
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
|
||||||
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -427,19 +427,20 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
|
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
|
||||||
{
|
{
|
||||||
string regex = @"^(D);(.+);([0-9]+);(allies|axis);(.+);([0-9]+);(allies|axis);(.+);(.+);([0-9]+);(.+);(.+)$";
|
// todo: maybe do something with this
|
||||||
var match = Regex.Match(eventLine, regex, RegexOptions.IgnoreCase);
|
//string regex = @"^(D);(.+);([0-9]+);(allies|axis);(.+);([0-9]+);(allies|axis);(.+);(.+);([0-9]+);(.+);(.+)$";
|
||||||
|
//var match = Regex.Match(eventLine, regex, RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
if (match.Success)
|
//if (match.Success)
|
||||||
{
|
//{
|
||||||
// this gives us what team the player is on
|
// // this gives us what team the player is on
|
||||||
var attackerStats = Servers[serverId].PlayerStats[attackerClientId];
|
// var attackerStats = Servers[serverId].PlayerStats[attackerClientId];
|
||||||
var victimStats = Servers[serverId].PlayerStats[victimClientId];
|
// var victimStats = Servers[serverId].PlayerStats[victimClientId];
|
||||||
IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString());
|
// IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString(), true);
|
||||||
IW4Info.Team attackerTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[7].ToString());
|
// IW4Info.Team attackerTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[7].ToString(), true);
|
||||||
attackerStats.Team = attackerTeam;
|
// attackerStats.Team = attackerTeam;
|
||||||
victimStats.Team = victimTeam;
|
// victimStats.Team = victimTeam;
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
using SharedLibraryCore.Database.Models;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace SharedLibraryCore.Commands
|
|
||||||
{
|
|
||||||
public class GenerateTokenCommand : Command
|
|
||||||
{
|
|
||||||
public GenerateTokenCommand() :
|
|
||||||
base("generatetoken", Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_DESC"], "gt", EFClient.Permission.Trusted, false)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public override Task ExecuteAsync(GameEvent E)
|
|
||||||
{
|
|
||||||
string token = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId);
|
|
||||||
var _event = token == null ?
|
|
||||||
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_FAIL"]) :
|
|
||||||
E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"]} {token}");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
20
SharedLibraryCore/Commands/RequestTokenCommand.cs
Normal file
20
SharedLibraryCore/Commands/RequestTokenCommand.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SharedLibraryCore.Commands
|
||||||
|
{
|
||||||
|
public class RequestTokenCommand : Command
|
||||||
|
{
|
||||||
|
public RequestTokenCommand() :
|
||||||
|
base("requesttoken", Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_DESC"], "rt", EFClient.Permission.Trusted, false)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public override Task ExecuteAsync(GameEvent E)
|
||||||
|
{
|
||||||
|
var state = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId);
|
||||||
|
E.Origin.Tell(string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token, $"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}", E.Origin.ClientId));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
SharedLibraryCore/Helpers/TokenState.cs
Normal file
15
SharedLibraryCore/Helpers/TokenState.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SharedLibraryCore.Helpers
|
||||||
|
{
|
||||||
|
public sealed class TokenState
|
||||||
|
{
|
||||||
|
public long NetworkId { get; set; }
|
||||||
|
public DateTime RequestTime { get; set; } = DateTime.Now;
|
||||||
|
public TimeSpan TokenDuration { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
public string RemainingTime => Math.Round(-(DateTime.Now - RequestTime).Subtract(TokenDuration).TotalMinutes, 1).ToString();
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using SharedLibraryCore.Helpers;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ namespace SharedLibraryCore.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="networkId">network id of the players to generate the token for</param>
|
/// <param name="networkId">network id of the players to generate the token for</param>
|
||||||
/// <returns>4 character string token</returns>
|
/// <returns>4 character string token</returns>
|
||||||
string GenerateNextToken(long networkId);
|
TokenState GenerateNextToken(long networkId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// authorizes given token
|
/// authorizes given token
|
||||||
|
@ -21,9 +21,11 @@ namespace WebfrontCore.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = Manager.GetPrivilegedClients()[clientId];
|
var client = Manager.GetPrivilegedClients()[clientId];
|
||||||
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(password, client.PasswordSalt));
|
|
||||||
|
|
||||||
if (hashedPassword[0] == client.Password)
|
// string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(password, client.PasswordSalt));
|
||||||
|
//if (hashedPassword[0] == client.Password)
|
||||||
|
|
||||||
|
if (Manager.TokenAuthenticator.AuthorizeToken(client.NetworkId, password))
|
||||||
{
|
{
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
@ -61,12 +63,5 @@ namespace WebfrontCore.Controllers
|
|||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
return RedirectToAction("Index", "Home");
|
return RedirectToAction("Index", "Home");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> GenerateLoginTokenAsync()
|
|
||||||
{
|
|
||||||
return Json(new { token = Manager.TokenAuthenticator.GenerateNextToken(Client.NetworkId) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using WebfrontCore.ViewModels;
|
using WebfrontCore.ViewModels;
|
||||||
@ -182,5 +183,25 @@ namespace WebfrontCore.Controllers
|
|||||||
command = $"!setlevel @{targetId} {level}"
|
command = $"!setlevel @{targetId} {level}"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> GenerateLoginTokenForm()
|
||||||
|
{
|
||||||
|
var info = new ActionInfo()
|
||||||
|
{
|
||||||
|
ActionButtonLabel = "Generate",
|
||||||
|
Name = "GenerateLoginToken",
|
||||||
|
Action = "GenerateLoginTokenAsync",
|
||||||
|
Inputs = new List<InputInfo>()
|
||||||
|
};
|
||||||
|
|
||||||
|
return View("_ActionForm", info);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public string GenerateLoginTokenAsync()
|
||||||
|
{
|
||||||
|
var state = Manager.TokenAuthenticator.GenerateNextToken(Client.NetworkId);
|
||||||
|
return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token, $"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}", Client.ClientId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,7 @@
|
|||||||
@class = "dropdown-item bg-dark text-muted text-center text-md-left",
|
@class = "dropdown-item bg-dark text-muted text-center text-md-left",
|
||||||
title = "Logout of account"
|
title = "Logout of account"
|
||||||
})
|
})
|
||||||
|
<a class="dropdown-item bg-dark text-muted text-center text-md-left profile-action" href="#" data-action="GenerateLoginToken" aria-hidden="true" title="@loc["WEBFRONT_ACTION_TOKEN"]">@loc["WEBFRONT_ACTION_TOKEN"]</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item text-center text-md-left"></li>
|
<li class="nav-item text-center text-md-left"></li>
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
<environment include="Development">
|
|
||||||
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
|
|
||||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
|
|
||||||
</environment>
|
|
||||||
<environment exclude="Development">
|
|
||||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"
|
|
||||||
asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
|
|
||||||
asp-fallback-test="window.jQuery && window.jQuery.validator"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
integrity="sha384-Fnqn3nxp3506LP/7Y3j/25BlWeA3PXTyT1l78LjECcPaKCV12TsZP7yyMxOe/G/k">
|
|
||||||
</script>
|
|
||||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"
|
|
||||||
asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
|
|
||||||
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
integrity="sha384-JrXK+k53HACyavUKOsL+NkmSesD2P+73eDMrbTtTk0h4RmOF8hF8apPlkp26JlyH">
|
|
||||||
</script>
|
|
||||||
</environment>
|
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
|
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
|
||||||
<RazorCompileOnBuild>true</RazorCompileOnBuild>
|
<RazorCompileOnBuild>false</RazorCompileOnBuild>
|
||||||
<RazorCompileOnPublish>true</RazorCompileOnPublish>
|
<RazorCompileOnPublish>true</RazorCompileOnPublish>
|
||||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||||
<TypeScriptToolsVersion>2.6</TypeScriptToolsVersion>
|
<TypeScriptToolsVersion>2.6</TypeScriptToolsVersion>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<Authors>RaidMax</Authors>
|
<Authors>RaidMax</Authors>
|
||||||
<Company>Forever None</Company>
|
<Company>Forever None</Company>
|
||||||
<Product>IW4MAdmin</Product>
|
<Product>IW4MAdmin</Product>
|
||||||
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server</Description>
|
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
|
||||||
<Copyright>2019</Copyright>
|
<Copyright>2019</Copyright>
|
||||||
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
|
||||||
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
|
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
|
||||||
@ -74,7 +74,5 @@
|
|||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
<ProjectExtensions><VisualStudio><UserProperties /></VisualStudio></ProjectExtensions>
|
<ProjectExtensions><VisualStudio><UserProperties /></VisualStudio></ProjectExtensions>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -6416,4 +6416,3 @@ form *, select {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1em;
|
bottom: 1em;
|
||||||
right: 1em; }
|
right: 1em; }
|
||||||
|
|
||||||
|
@ -178,7 +178,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.profile-action {
|
.profile-action {
|
||||||
padding-left: 0.25em;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user