1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-07 21:58:06 -05:00
IW4M-Admin/WebfrontCore/Controllers/ActionController.cs
Amos cba9f1726b Utilise Minutes in TempBan for under 1 hour bans. (#362)
* Update tempban command to use minutes instead of hours

The duration calculation for tempbans now uses minutes rather than hours for greater precision. This change ensures better alignment with user expectations and configurations in smaller timeframes.

* Update regex patterns to enforce stricter input validation

Adjusted regex in `ParseTimespan` and `TempBanCommand` to ensure input starts with 1-5 numeric characters, improving validation. This prevents malformed user inputs and enhances overall command reliability.

* \w includes digits. Who knew.
2025-05-20 21:37:04 -05:00

878 lines
32 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.ViewModels;
namespace WebfrontCore.Controllers
{
public class ActionController : BaseController
{
private readonly ApplicationConfiguration _appConfig;
private readonly IMetaServiceV2 _metaService;
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private readonly ITranslationLookup _translationLookup;
private readonly string _banCommandName;
private readonly string _tempbanCommandName;
private readonly string _unbanCommandName;
private readonly string _sayCommandName;
private readonly string _kickCommandName;
private readonly string _offlineMessageCommandName;
private readonly string _flagCommandName;
private readonly string _unflagCommandName;
private readonly string _setLevelCommandName;
private readonly string _setClientTagCommandName;
private readonly string _addClientNoteCommandName;
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
ApplicationConfiguration appConfig, IMetaServiceV2 metaService,
IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService,
ITranslationLookup translationLookup) : base(manager)
{
_appConfig = appConfig;
_metaService = metaService;
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
_translationLookup = translationLookup;
foreach (var cmd in registeredCommands)
{
var type = cmd.GetType().Name;
switch (type)
{
case nameof(BanCommand):
_banCommandName = cmd.Name;
break;
case nameof(TempBanCommand):
_tempbanCommandName = cmd.Name;
break;
case nameof(UnbanCommand):
_unbanCommandName = cmd.Name;
break;
// todo: this should be flag driven
case "SayCommand":
_sayCommandName = cmd.Name;
break;
case nameof(KickCommand):
_kickCommandName = cmd.Name;
break;
case nameof(FlagClientCommand):
_flagCommandName = cmd.Name;
break;
case nameof(UnflagClientCommand):
_unflagCommandName = cmd.Name;
break;
case nameof(SetLevelCommand):
_setLevelCommandName = cmd.Name;
break;
case "OfflineMessageCommand":
_offlineMessageCommandName = cmd.Name;
break;
case "SetClientTagCommand":
_setClientTagCommandName = cmd.Name;
break;
case "AddClientNoteCommand":
_addClientNoteCommandName = cmd.Name;
break;
}
}
}
public IActionResult DynamicActionForm(int? id, string meta)
{
if (Client.ClientId < 1)
{
return Ok(new[]
{
new CommandResponseInfo
{
Response = _translationLookup["SERVER_COMMANDS_INTERCEPTED"]
}
});
}
if (meta is null)
{
return BadRequest(new[]
{
new CommandResponseInfo
{
Response = "INVALID"
}
});
}
var metaDict = JsonSerializer.Deserialize<Dictionary<string, string>>(meta.TrimEnd('"').TrimStart('"'));
if (metaDict is null)
{
return BadRequest();
}
metaDict.TryGetValue(nameof(ActionInfo.ActionButtonLabel), out var label);
metaDict.TryGetValue(nameof(ActionInfo.Name), out var name);
metaDict.TryGetValue(nameof(ActionInfo.ShouldRefresh), out var refresh);
metaDict.TryGetValue("Data", out var data);
metaDict.TryGetValue("InteractionId", out var interactionId);
metaDict.TryGetValue("Inputs", out var template);
List<InputInfo> additionalInputs = null;
var inputKeys = string.Empty;
if (!string.IsNullOrWhiteSpace(template))
{
additionalInputs = JsonSerializer.Deserialize<List<InputInfo>>(template);
}
if (additionalInputs is not null)
{
inputKeys = string.Join(",", additionalInputs.Select(input => input.Name));
}
bool.TryParse(refresh, out var shouldRefresh);
var inputs = new List<InputInfo>
{
new()
{
Name = "InteractionId",
Value = interactionId,
Type = "hidden"
},
new()
{
Name = "data",
Value = data,
Type = "hidden"
},
new()
{
Name = "TargetId",
Value = id?.ToString(),
Type = "hidden"
},
new()
{
Name = "CustomInputKeys",
Value = inputKeys,
Type = "hidden"
}
};
if (additionalInputs?.Any() ?? false)
{
inputs.AddRange(additionalInputs);
}
var info = new ActionInfo
{
ActionButtonLabel = label,
Name = name,
Action = nameof(DynamicActionAsync),
ShouldRefresh = shouldRefresh,
Inputs = inputs
};
return View("_ActionForm", info);
}
public async Task<IActionResult> DynamicActionAsync(CancellationToken token = default)
{
if (Client.ClientId < 1)
{
return Ok(new[]
{
new CommandResponseInfo
{
Response = _translationLookup["SERVER_COMMANDS_INTERCEPTED"]
}
});
}
HttpContext.Request.Query.TryGetValue("InteractionId", out var interactionId);
HttpContext.Request.Query.TryGetValue("CustomInputKeys", out var inputKeys);
HttpContext.Request.Query.TryGetValue("Data", out var data);
HttpContext.Request.Query.TryGetValue("TargetId", out var targetIdString);
var inputs = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(inputKeys.ToString()))
{
foreach (var key in inputKeys.ToString().Split(","))
{
HttpContext.Request.Query.TryGetValue(key, out var input);
if (string.IsNullOrWhiteSpace(input))
{
continue;
}
inputs.Add(key, HttpContext.Request.Query[key]);
}
}
var game = (Reference.Game?)null;
var targetId = (int?)null;
if (int.TryParse(targetIdString.ToString().Split(",").Last(), out var parsedTargetId))
{
targetId = parsedTargetId;
}
if (targetId.HasValue)
{
game = (await Manager.GetClientService().Get(targetId.Value))?.GameName;
}
if (interactionId.ToString() != "command")
{
return Ok(await _interactionRegistration.ProcessInteraction(interactionId, Client.ClientId, targetId, game, inputs,
token));
}
var server = Manager.GetServers().First();
var (success, result) = await _remoteCommandService.ExecuteWithResult(Client.ClientId, targetId, data,
inputs.Values.Select(input => input), server);
return success ? Ok(result) : BadRequest(result);
}
public IActionResult BanForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_BAN_NAME"],
Name = Localization["WEBFRONT_ACTION_BAN_NAME"],
Inputs = new List<InputInfo>
{
new()
{
Name = "Reason",
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
},
new()
{
Name = "PresetReason",
Type = "select",
Label = Localization["WEBFRONT_ACTION_LABEL_PRESET_REASON"],
Values = GetPresetPenaltyReasons()
},
new()
{
Name = "Duration",
Label = Localization["WEBFRONT_ACTION_LABEL_DURATION"],
Type = "select",
Values = _appConfig.BanDurations
.Select((item, index) => new
{
Id = (index + 1).ToString(),
Value = item.HumanizeForCurrentCulture()
}
)
.Append(new
{
Id = (_appConfig.BanDurations.Length + 1).ToString(),
Value = Localization["WEBFRONT_ACTION_SELECTION_PERMANENT"]
}).ToDictionary(duration => duration.Id, duration => duration.Value),
}
},
Action = "BanAsync",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> BanAsync(int targetId, string reason, int duration, string presetReason = null)
{
var fallthroughReason = presetReason ?? reason;
string command;
// permanent
if (duration > _appConfig.BanDurations.Length)
{
command = $"{_appConfig.CommandPrefix}{_banCommandName} @{targetId} {fallthroughReason}";
}
// temporary ban
else
{
var durationSpan = _appConfig.BanDurations[duration - 1];
var durationValue = durationSpan.TotalMinutes.ToString(CultureInfo.InvariantCulture) +
Localization["GLOBAL_TIME_MINUTES"][0];
command =
$"{_appConfig.CommandPrefix}{_tempbanCommandName} @{targetId} {durationValue} {fallthroughReason}";
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command
}));
}
public IActionResult UnbanForm(long? id)
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Name = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Inputs = new List<InputInfo>
{
new()
{
Name = "Reason",
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
}
},
Action = "UnbanAsync",
ShouldRefresh = true
};
if (id is not null)
{
info.Inputs.Add(new()
{
Name = "targetId",
Value = id.ToString(),
Type = "hidden"
});
}
return View("_ActionForm", info);
}
public async Task<IActionResult> UnbanAsync(int targetId, string reason)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command = $"{_appConfig.CommandPrefix}{_unbanCommandName} @{targetId} {reason}"
}));
}
public IActionResult LoginForm()
{
var login = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LOGIN_NAME"],
Name = Localization["WEBFRONT_ACTION_LOGIN_NAME"],
Inputs = new List<InputInfo>
{
new()
{
Name = "clientId",
Label = Localization["WEBFRONT_ACTION_LABEL_ID"],
Required = true
},
new()
{
Name = "Password",
Label = Localization["WEBFRONT_ACTION_LABEL_PASSWORD"],
Type = "password",
Required = true
}
},
Action = "Login"
};
return View("_ActionForm", login);
}
public async Task<IActionResult> Login(int clientId, string password)
{
return await Task.FromResult(RedirectToAction("Login", "Account", new { clientId, password }));
}
public IActionResult EditForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_EDIT"],
Name = Localization["WEBFRONT_ACTION_LABEL_EDIT"],
Inputs = new List<InputInfo>
{
new()
{
Name = "level",
Label = Localization["WEBFRONT_PROFILE_LEVEL"],
Type = "select",
Values = Enum.GetValues(typeof(EFClient.Permission)).OfType<EFClient.Permission>()
.Where(p => p <= Client.Level)
.Where(p => p != EFClient.Permission.Banned)
.Where(p => p != EFClient.Permission.Flagged)
.ToDictionary(p => p.ToString(), p => p.ToLocalizedLevelName())
},
},
Action = "EditAsync",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> EditAsync(int targetId, string level)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command = $"{_appConfig.CommandPrefix}{_setLevelCommandName} @{targetId} {level}"
}));
}
public IActionResult GenerateLoginTokenForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_GENERATE_TOKEN"],
Name = "GenerateLoginToken",
Action = "GenerateLoginTokenAsync",
Inputs = new List<InputInfo>()
};
return View("_ActionForm", info);
}
[Authorize]
public string GenerateLoginTokenAsync()
{
var state = Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
{
ClientId = Client.ClientId
});
return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"],
state.Token,
$"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}",
Client.ClientId);
}
public IActionResult ChatForm(long id)
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"],
Name = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "message",
Type = "text",
Label = Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]
},
new()
{
Name = "id",
Value = id.ToString(),
Type = "hidden"
}
},
Action = "ChatAsync"
};
return View("_ActionForm", info);
}
public async Task<IActionResult> ChatAsync(long id, string message)
{
var server = Manager.GetServers().First(server => server.EndPoint == id);
server.ChatHistory.Add(new SharedLibraryCore.Dtos.ChatInfo()
{
ClientId = Client.ClientId,
Message = message,
Name = Client.Name,
ServerGame = server.GameName,
Time = DateTime.Now
});
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command = $"{_appConfig.CommandPrefix}{_sayCommandName} {message}"
}));
}
public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
{
ViewBag.First = request.Offset == 0;
if (request.Count > 20)
{
request.Count = 20;
}
var clients = await Manager.GetClientService().GetRecentClients(request);
return request.Offset == 0
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
}
public IActionResult RecentReportsForm()
{
var serverInfo = Manager.GetServers().Select(server =>
new ServerInfo
{
Name = server.Hostname,
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24)
.ToList()
});
return View("Partials/_Reports", serverInfo);
}
public IActionResult FlagForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_FLAG_NAME"],
Name = Localization["WEBFRONT_ACTION_FLAG_NAME"],
Inputs = new List<InputInfo>
{
new()
{
Name = "reason",
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
},
new()
{
Name = "PresetReason",
Type = "select",
Label = Localization["WEBFRONT_ACTION_LABEL_PRESET_REASON"],
Values = GetPresetPenaltyReasons()
},
},
Action = "FlagAsync",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> FlagAsync(int targetId, string reason, string presetReason = null)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command = $"{_appConfig.CommandPrefix}{_flagCommandName} @{targetId} {presetReason ?? reason}"
}));
}
public IActionResult UnflagForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNFLAG_NAME"],
Name = Localization["WEBFRONT_ACTION_UNFLAG_NAME"],
Inputs = new List<InputInfo>
{
new()
{
Name = "reason",
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
}
},
Action = "UnflagAsync",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> UnflagAsync(int targetId, string reason)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command = $"{_appConfig.CommandPrefix}{_unflagCommandName} @{targetId} {reason}"
}));
}
public IActionResult KickForm(int id)
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_KICK_NAME"],
Name = Localization["WEBFRONT_ACTION_KICK_NAME"],
Inputs = new List<InputInfo>
{
new()
{
Name = "reason",
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
},
new()
{
Name = "PresetReason",
Type = "select",
Label = Localization["WEBFRONT_ACTION_LABEL_PRESET_REASON"],
Values = GetPresetPenaltyReasons()
},
new()
{
Name = "targetId",
Type = "hidden",
Value = id.ToString()
}
},
Action = "KickAsync",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> KickAsync(int targetId, string reason, string presetReason = null)
{
var client = Manager.GetActiveClients().FirstOrDefault(client => client.ClientId == targetId);
if (client == null)
{
return BadRequest(Localization["WEBFRONT_ACTION_KICK_DISCONNECT"]);
}
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = client.CurrentServer.EndPoint,
command = $"{_appConfig.CommandPrefix}{_kickCommandName} {client.ClientNumber} {presetReason ?? reason}"
}));
}
public IActionResult DismissAlertForm(Guid id)
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "alertId",
Type = "hidden",
Value = id.ToString()
}
},
Action = nameof(DismissAlert),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAlert(Guid alertId)
{
AlertManager.MarkAlertAsRead(alertId);
return Json(new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE_RESPONSE"]
}
});
}
public IActionResult DismissAllAlertsForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY"],
Inputs = new List<InputInfo>
{
new()
{
Name = "targetId",
Type = "hidden",
Value = Client.ClientId.ToString()
}
},
Action = nameof(DismissAllAlerts),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAllAlerts(int targetId)
{
AlertManager.MarkAllAlertsAsRead(targetId);
return Json(new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY_RESPONSE"]
}
});
}
public IActionResult OfflineMessageForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_BUTTON_COMPOSE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "message",
Label = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_CONTENT"],
},
},
Action = "OfflineMessage",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> OfflineMessage(int targetId, string message)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_offlineMessageCommandName} @{targetId} {message.CapClientName(500)}"
}));
}
public async Task<IActionResult> SetClientTagForm(int id, CancellationToken token)
{
var tags = await _metaService.GetPersistentMetaValue<List<LookupValue<string>>>(EFMeta.ClientTagNameV2,
token) ?? new List<LookupValue<string>>();
var existingTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, id, Manager.CancellationToken);
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_SUBMIT"],
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TAG"],
Inputs = new List<InputInfo>
{
new()
{
Name = "clientTag",
Type = "select",
Label = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_FORM_TAG"],
Values = tags.ToDictionary(
item => item.Value == existingTag?.Value ? $"!selected!{item.Value}" : item.Value,
item => item.Value)
}
},
Action = nameof(SetClientTag),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> SetClientTag(int targetId, string clientTag)
{
if (targetId <= 0 || string.IsNullOrWhiteSpace(clientTag))
{
return Json(new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_NONE"]
}
});
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_setClientTagCommandName} @{targetId} {clientTag}"
}));
}
public async Task<IActionResult> AddClientNoteForm(int id)
{
var existingNote = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", id);
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_CONFIGURATION_BUTTON_SAVE"],
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_NOTE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "note",
Label = Localization["WEBFRONT_ACTION_NOTE_FORM_NOTE"],
Value = existingNote?.Note,
Type = "textarea"
}
},
Action = nameof(AddClientNote),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> AddClientNote(int targetId, string note)
{
if (note?.Length > 350 || note?.Count(c => c == '\n') > 4)
{
return StatusCode(StatusCodes.Status400BadRequest, new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_NOTE_INVALID_LENGTH"]
}
});
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_addClientNoteCommandName} @{targetId} {note}"
}));
}
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
.Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
.Select((value, _) => new
{
Value = value
})
// this is used for the default empty optional value
.Prepend(new
{
Value = ""
})
.Distinct()
.ToDictionary(item => item.Value, item => item.Value);
}
}