Logging for Genesis toolbox
This commit is contained in:
@@ -155,7 +155,7 @@
|
||||
</site>
|
||||
<site name="MeterProcessState" id="2">
|
||||
<application path="/" applicationPool="Clr4IntegratedAppPool">
|
||||
<virtualDirectory path="/" physicalPath="D:\Projekte\SENSUS_GitLab\laa_production\Common\Service\MeterProcessState" />
|
||||
<virtualDirectory path="/" physicalPath="C:\Users\SZLATEV\source\repos\laa_production\Common\Service\MeterProcessState" />
|
||||
</application>
|
||||
<bindings>
|
||||
<binding protocol="http" bindingInformation="*:56011:localhost" />
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
encoding: Encoding.UTF8,
|
||||
content: JsonConvert.SerializeObject(new
|
||||
{
|
||||
windowsUser = Environment.UserName,
|
||||
software,
|
||||
functions
|
||||
}));
|
||||
@@ -78,6 +79,7 @@
|
||||
appId,
|
||||
username,
|
||||
password,
|
||||
windowsUser = Environment.UserName,
|
||||
}));
|
||||
|
||||
using (var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage))
|
||||
@@ -101,5 +103,36 @@
|
||||
.ConfigureAwait(true)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
internal static void LogAction(string buttonText, string buttonTag, string user, int appId)
|
||||
=> Task
|
||||
.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, "api/software/functions"))
|
||||
{
|
||||
httpRequestMessage.Content = new StringContent(
|
||||
mediaType: APPLICATION_JSON,
|
||||
encoding: Encoding.UTF8,
|
||||
content: JsonConvert.SerializeObject(new
|
||||
{
|
||||
WindowsUser = Environment.UserName,
|
||||
Action = "ACCESS",
|
||||
AppId = appId,
|
||||
Message = $"User '{user}' accessed '{buttonText}' with Tag: {buttonTag}"
|
||||
}));
|
||||
|
||||
_ = await httpClient.SendAsync(httpRequestMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// TODO: Log the error message.
|
||||
}
|
||||
})
|
||||
.ConfigureAwait(true)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
{
|
||||
internal const string UNAVAILABLE = "Genesis Toolbox is currently not available.";
|
||||
|
||||
|
||||
private static IEnumerable<SoftwareFunctions> softwareFunctions;
|
||||
|
||||
internal static int Id { get; private set; }
|
||||
|
||||
internal static string User { get; private set; }
|
||||
|
||||
internal static bool IsAuthorized { get; private set; }
|
||||
|
||||
internal static bool Initialized { get; private set; }
|
||||
@@ -40,6 +41,7 @@
|
||||
{
|
||||
softwareFunctions = ApplicationHttp.GetSoftwareFunction<SoftwareFunctions>(Id, username, password);
|
||||
IsAuthorized = softwareFunctions != null;
|
||||
User = username;
|
||||
|
||||
if (!IsAuthorized)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
[STAThread]
|
||||
internal static void Main()
|
||||
{
|
||||
// Initializes application state for the hole application as static properties and methods.
|
||||
ApplicationState.Initialize();
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new FrmMainForm());
|
||||
|
||||
@@ -41,95 +41,6 @@ namespace Xylem.Common.Ui.GenesisToolBox
|
||||
lblVersionNumber.Text = $@"Version: {version.Major}.{version.Minor}.{version.Build}";
|
||||
}
|
||||
|
||||
private void InitializeFormState()
|
||||
{
|
||||
foreach (var control in this.Controls)
|
||||
{
|
||||
if (control is GroupBox groupBox)
|
||||
{
|
||||
groupBox.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
this.btnSetup.Tag = SoftwareFunctions.GENERAL_SETUP;
|
||||
this.btnAlarm.Tag = SoftwareFunctions.GENERAL_ALARM;
|
||||
this.btnRegisterStore.Tag = SoftwareFunctions.GENERAL_REGISTER_STORE;
|
||||
this.btnTempLogger.Tag = SoftwareFunctions.GENERAL_TEMP_METER;
|
||||
this.btnStreamingQuality.Tag = SoftwareFunctions.GENERAL_STREAM_QUALITY;
|
||||
this.btnFwUpdate.Tag = SoftwareFunctions.GENERAL_FIRMWARE_UPDATE;
|
||||
this.btnLutUpdate.Tag = SoftwareFunctions.GENERAL_LUT_UPDATE;
|
||||
this.button4.Tag = SoftwareFunctions.GENERAL_LUT_MANAGEMENT;
|
||||
this.btnPressure.Tag = SoftwareFunctions.GENERAL_PRESSURE_LOG;
|
||||
this.button1.Tag = SoftwareFunctions.GENERAL_PULS_CONFIG;
|
||||
|
||||
this.btnPassword.Tag = SoftwareFunctions.LAA_PASSWORD;
|
||||
this.btnRegHistory.Tag = SoftwareFunctions.LAA_REGISTER_HISTORY;
|
||||
this.btnPara.Tag = SoftwareFunctions.LAA_PARAMETERIZATION;
|
||||
this.button2.Tag = SoftwareFunctions.LAA_PROCESS_PARAMETER;
|
||||
this.button5.Tag = SoftwareFunctions.LAA_MAPPING_NA;
|
||||
|
||||
this.btnLegacyTestApp.Tag = SoftwareFunctions.DEV_LEGACY_TEST_APP;
|
||||
this.btnFiles.Tag = SoftwareFunctions.DEV_FILE_CONTROL;
|
||||
this.BtnFileSplit.Tag = SoftwareFunctions.DEV_FILE_SPLIT;
|
||||
this.btnLowLevelTools.Tag = SoftwareFunctions.DEV_LOW_LEVEL_TOOLS;
|
||||
|
||||
if (!ApplicationState.Initialized)
|
||||
{
|
||||
this.Controls.Add(new Label()
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Text = ApplicationState.UNAVAILABLE,
|
||||
TextAlign = ContentAlignment.MiddleCenter,
|
||||
Font = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Bold),
|
||||
ForeColor = Color.DarkRed,
|
||||
Padding = new Padding(15)
|
||||
});
|
||||
}
|
||||
else if (!ApplicationState.IsAuthorized)
|
||||
{
|
||||
this.Controls.Add(new LoginForm(this.LoginFormSubmitted));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ShowAvailableSoftwareFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoginFormSubmitted(string username, string password, object sender)
|
||||
{
|
||||
if (sender is LoginForm loginForm)
|
||||
{
|
||||
var errorMessage = default(string);
|
||||
|
||||
if (ApplicationState.Authorize(username, password, out errorMessage))
|
||||
{
|
||||
this.Controls.Remove(loginForm);
|
||||
this.ShowAvailableSoftwareFunctions();
|
||||
}
|
||||
|
||||
loginForm.ShowError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowAvailableSoftwareFunctions()
|
||||
{
|
||||
foreach (var control in this.Controls)
|
||||
{
|
||||
if (control is GroupBox groupBox)
|
||||
{
|
||||
foreach (var groupBoxControl in groupBox.Controls)
|
||||
{
|
||||
if (groupBoxControl is Button button && button.Tag is SoftwareFunctions softwareFunction)
|
||||
{
|
||||
button.Enabled = ApplicationState.IsEnabled(softwareFunction);
|
||||
}
|
||||
}
|
||||
|
||||
groupBox.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void btnSetup_Click(Object sender, EventArgs e)
|
||||
{
|
||||
var form = new FrmSetup();
|
||||
@@ -481,5 +392,125 @@ namespace Xylem.Common.Ui.GenesisToolBox
|
||||
form.Closed += Form_Closed;
|
||||
Hide();
|
||||
}
|
||||
|
||||
#region Form initialization
|
||||
|
||||
private void HideContent()
|
||||
{
|
||||
foreach (var control in this.Controls)
|
||||
{
|
||||
if (control is GroupBox groupBox)
|
||||
{
|
||||
groupBox.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSoftwareFunctions()
|
||||
{
|
||||
this.btnSetup.Tag = SoftwareFunctions.GENERAL_SETUP;
|
||||
this.btnAlarm.Tag = SoftwareFunctions.GENERAL_ALARM;
|
||||
this.btnRegisterStore.Tag = SoftwareFunctions.GENERAL_REGISTER_STORE;
|
||||
this.btnTempLogger.Tag = SoftwareFunctions.GENERAL_TEMP_METER;
|
||||
this.btnStreamingQuality.Tag = SoftwareFunctions.GENERAL_STREAM_QUALITY;
|
||||
this.btnFwUpdate.Tag = SoftwareFunctions.GENERAL_FIRMWARE_UPDATE;
|
||||
this.btnLutUpdate.Tag = SoftwareFunctions.GENERAL_LUT_UPDATE;
|
||||
this.button4.Tag = SoftwareFunctions.GENERAL_LUT_MANAGEMENT;
|
||||
this.btnPressure.Tag = SoftwareFunctions.GENERAL_PRESSURE_LOG;
|
||||
this.button1.Tag = SoftwareFunctions.GENERAL_PULS_CONFIG;
|
||||
|
||||
this.btnPassword.Tag = SoftwareFunctions.LAA_PASSWORD;
|
||||
this.btnRegHistory.Tag = SoftwareFunctions.LAA_REGISTER_HISTORY;
|
||||
this.btnPara.Tag = SoftwareFunctions.LAA_PARAMETERIZATION;
|
||||
this.button2.Tag = SoftwareFunctions.LAA_PROCESS_PARAMETER;
|
||||
this.button5.Tag = SoftwareFunctions.LAA_MAPPING_NA;
|
||||
|
||||
this.btnLegacyTestApp.Tag = SoftwareFunctions.DEV_LEGACY_TEST_APP;
|
||||
this.btnFiles.Tag = SoftwareFunctions.DEV_FILE_CONTROL;
|
||||
this.BtnFileSplit.Tag = SoftwareFunctions.DEV_FILE_SPLIT;
|
||||
this.btnLowLevelTools.Tag = SoftwareFunctions.DEV_LOW_LEVEL_TOOLS;
|
||||
}
|
||||
|
||||
private void ShowErrorMessage()
|
||||
{
|
||||
this.Controls.Add(new Label()
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Text = ApplicationState.UNAVAILABLE,
|
||||
TextAlign = ContentAlignment.MiddleCenter,
|
||||
Font = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Bold),
|
||||
ForeColor = Color.DarkRed,
|
||||
Padding = new Padding(15)
|
||||
});
|
||||
}
|
||||
|
||||
private void LoginFormSubmitted(string username, string password, object sender)
|
||||
{
|
||||
if (sender is LoginForm loginForm)
|
||||
{
|
||||
var errorMessage = default(string);
|
||||
|
||||
if (ApplicationState.Authorize(username, password, out errorMessage))
|
||||
{
|
||||
this.Controls.Remove(loginForm);
|
||||
this.ShowAvailableSoftwareFunctions();
|
||||
}
|
||||
|
||||
loginForm.ShowError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowAvailableSoftwareFunctions()
|
||||
{
|
||||
foreach (var control in this.Controls)
|
||||
{
|
||||
if (control is GroupBox groupBox)
|
||||
{
|
||||
foreach (var groupBoxControl in groupBox.Controls)
|
||||
{
|
||||
if (groupBoxControl is Button button)
|
||||
{
|
||||
if (button.Tag is SoftwareFunctions softwareFunction)
|
||||
{
|
||||
button.Enabled = ApplicationState.IsEnabled(softwareFunction);
|
||||
}
|
||||
|
||||
button.Click += LogUserAction;
|
||||
}
|
||||
}
|
||||
|
||||
groupBox.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LogUserAction(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
ApplicationHttp.LogAction(button.Text, $"{button.Tag}", ApplicationState.User, ApplicationState.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeFormState()
|
||||
{
|
||||
this.HideContent();
|
||||
this.SetSoftwareFunctions();
|
||||
|
||||
if (!ApplicationState.Initialized)
|
||||
{
|
||||
this.ShowErrorMessage();
|
||||
}
|
||||
else if (!ApplicationState.IsAuthorized)
|
||||
{
|
||||
this.Controls.Add(new LoginForm(this.LoginFormSubmitted));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ShowAvailableSoftwareFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Form initialization
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,9 @@
|
||||
.ExecuteReader(x => x.GetString());
|
||||
|
||||
public short Login(string username, string password)
|
||||
=> this.sqlClient.FirstOrDefault($@"
|
||||
{
|
||||
var userId = this.sqlClient
|
||||
.FirstOrDefault($@"
|
||||
SELECT TOP 1 [MitarbeiterNr]
|
||||
FROM [Mitarbeiter]
|
||||
WHERE [Benutzername] LIKE @{nameof(username)}
|
||||
@@ -62,6 +64,8 @@
|
||||
.Add(nameof(username), username)
|
||||
.Add(nameof(password), password),
|
||||
reader => reader.GetSmallint());
|
||||
return userId;
|
||||
}
|
||||
|
||||
public int ResetPassword(string username)
|
||||
=> this.sqlClient
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace LaaProductionWeb.Services.Interfaces
|
||||
{
|
||||
using System;
|
||||
|
||||
public interface ILoggerService
|
||||
{
|
||||
void Log(Action<LoggerService.LogModel> logModel);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
void DeleteFunction(int funcId);
|
||||
|
||||
short FindAndUpdateSoftware(string name, IEnumerable<string> functions);
|
||||
short FindAndUpdateSoftware(string windowsUser, string software, IEnumerable<string> functions);
|
||||
|
||||
IEnumerable<SoftwareFunction> GetAllSoftwareFunctions();
|
||||
|
||||
|
||||
@@ -61,11 +61,13 @@
|
||||
<Compile Include="Interfaces\IAccountService.cs" />
|
||||
<Compile Include="Interfaces\IApprovalsService.cs" />
|
||||
<Compile Include="Interfaces\IHttpService.cs" />
|
||||
<Compile Include="Interfaces\ILoggerService.cs" />
|
||||
<Compile Include="Interfaces\IOrdersService.cs" />
|
||||
<Compile Include="Interfaces\IProtocolService.cs" />
|
||||
<Compile Include="Interfaces\IReportService.cs" />
|
||||
<Compile Include="Interfaces\IShipmentsService.cs" />
|
||||
<Compile Include="Interfaces\ISoftwareService.cs" />
|
||||
<Compile Include="LoggerService.cs" />
|
||||
<Compile Include="Models\Employee.cs" />
|
||||
<Compile Include="Models\HttpResponseModel.cs" />
|
||||
<Compile Include="Models\MissingItem.cs" />
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace LaaProductionWeb.Services
|
||||
{
|
||||
using LaaProductionWeb.Data.Interfaces;
|
||||
using LaaProductionWeb.Services.Interfaces;
|
||||
|
||||
using System;
|
||||
|
||||
public class LoggerService : ILoggerService
|
||||
{
|
||||
private readonly ISqlClient sqlClient;
|
||||
|
||||
public LoggerService(ISqlClient sqlClient)
|
||||
=> this.sqlClient = sqlClient;
|
||||
|
||||
public class Actions
|
||||
{
|
||||
public const string RUN = nameof(RUN);
|
||||
}
|
||||
|
||||
public class LogModel
|
||||
{
|
||||
public long Id { get; internal set; }
|
||||
|
||||
public DateTime Date { get; internal set; }
|
||||
= DateTime.Now;
|
||||
|
||||
public string User { get; set; }
|
||||
|
||||
public string Action { get; set; }
|
||||
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public void Log(Action<LogModel> logModel)
|
||||
{
|
||||
var model = new LogModel();
|
||||
logModel?.Invoke(model);
|
||||
|
||||
this.sqlClient
|
||||
.CreateCommand($@"
|
||||
INSERT INTO [SoftwareFunctionsLog] (
|
||||
[User]
|
||||
, [Action]
|
||||
, [Data])
|
||||
VALUES (@{nameof(model.User)}
|
||||
, @{nameof(model.Action)}
|
||||
, @{nameof(model.Data)})")
|
||||
.AddParameter(nameof(model.User), model.User)
|
||||
.AddParameter(nameof(model.Action), model.Action)
|
||||
.AddParameter(nameof(model.Data), model.Data)
|
||||
.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public short FindAndUpdateSoftware(string name, IEnumerable<string> functions)
|
||||
public short FindAndUpdateSoftware(string windowsUser, string name, IEnumerable<string> functions)
|
||||
{
|
||||
var appId = this.sqlClient
|
||||
.CreateCommand($@"
|
||||
@@ -67,8 +67,11 @@
|
||||
.AddParameter(nameof(name), name)
|
||||
.FirstOrDefault(x => x.GetSmallint());
|
||||
|
||||
if (appId > 0 && functions != null && functions.Any())
|
||||
if (appId > 0)
|
||||
{
|
||||
if (functions != null && functions.Any())
|
||||
{
|
||||
|
||||
foreach (var function in functions)
|
||||
{
|
||||
if (Regex.IsMatch(function, "[a-zA-Z_]+"))
|
||||
@@ -77,6 +80,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appId;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
services.AddScoped<IApprovalsService, ApprovalsService>();
|
||||
services.AddScoped<ILoggerService, LoggerService>();
|
||||
services.AddScoped<IOrdersService, OrdersService>();
|
||||
services.AddScoped<IProtocolService, ProtocolService>();
|
||||
services.AddScoped<IReportService, ReportService>();
|
||||
|
||||
@@ -2,23 +2,41 @@
|
||||
{
|
||||
using LaaProductionWeb.Services.Interfaces;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Http;
|
||||
|
||||
public class AppIdController : ApiController
|
||||
{
|
||||
private readonly ISoftwareService softwareService;
|
||||
private readonly ILoggerService loggerService;
|
||||
|
||||
public AppIdController()
|
||||
=> this.softwareService = ServiceProvider.Current.GetService<ISoftwareService>();
|
||||
{
|
||||
this.softwareService = ServiceProvider.Current.GetService<ISoftwareService>();
|
||||
this.loggerService = ServiceProvider.Current.GetService<ILoggerService>();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public int Post(AppIdPostInputModel model)
|
||||
=> this.softwareService.FindAndUpdateSoftware(model.Software, model.Functions);
|
||||
{
|
||||
var appId = this.softwareService.FindAndUpdateSoftware(model.WindowsUser, model.Software, model.Functions);
|
||||
|
||||
this.loggerService.Log(x =>
|
||||
{
|
||||
x.User = model.WindowsUser;
|
||||
x.Action = "STARTING APPLICATION";
|
||||
x.Data = $"AppId: {appId}, Functions: {string.Join(", ", model.Functions ?? Array.Empty<string>())}";
|
||||
});
|
||||
|
||||
return appId;
|
||||
}
|
||||
}
|
||||
|
||||
public class AppIdPostInputModel
|
||||
{
|
||||
public string WindowsUser { get; set; }
|
||||
|
||||
public string Software { get; set; }
|
||||
|
||||
public IEnumerable<string> Functions { get; set; }
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
{
|
||||
using LaaProductionWeb.Services.Interfaces;
|
||||
|
||||
using System;
|
||||
using System.Web.Http;
|
||||
|
||||
public class FunctionsController : ApiController
|
||||
{
|
||||
private readonly IAccountService accountService;
|
||||
private readonly ISoftwareService softwareService;
|
||||
private readonly ILoggerService loggerService;
|
||||
|
||||
public FunctionsController()
|
||||
{
|
||||
this.accountService = ServiceProvider.Current.GetService<IAccountService>();
|
||||
this.softwareService = ServiceProvider.Current.GetService<ISoftwareService>();
|
||||
this.loggerService = ServiceProvider.Current.GetService<ILoggerService>();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -22,21 +25,61 @@
|
||||
|
||||
if (userId <= 0)
|
||||
{
|
||||
this.loggerService.Log(options =>
|
||||
{
|
||||
options.User = model.WindowsUser;
|
||||
options.Action = "LOG IN FAILED";
|
||||
options.Data = $"{model.WindowsUser}:{model.AppId} - for user {model.Username} with password {model.Password}";
|
||||
});
|
||||
|
||||
return this.Unauthorized();
|
||||
}
|
||||
|
||||
var functions = this.softwareService.GetSoftwareFunctions(model.AppId, userId);
|
||||
|
||||
this.loggerService.Log(options =>
|
||||
{
|
||||
options.User = model.WindowsUser;
|
||||
options.Action = "LOG IN SUCCEEDED";
|
||||
options.Data = $"{model.WindowsUser}:{model.AppId} - for user {model.Username} with password {model.Password}. User has following functions {string.Join(", ", functions ?? Array.Empty<string>())}";
|
||||
});
|
||||
|
||||
return this.Json(functions);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public IHttpActionResult Put(FunctionsPutInputModel model)
|
||||
{
|
||||
this.loggerService.Log(options =>
|
||||
{
|
||||
options.User = model.WindowsUser;
|
||||
options.Action = model.Action;
|
||||
options.Data = $"AppId: {model.AppId} - reports: {model.Message}";
|
||||
});
|
||||
|
||||
return this.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public class FunctionsPostInputModel
|
||||
{
|
||||
public short AppId { get; set; }
|
||||
|
||||
public string WindowsUser { get; set; }
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public class FunctionsPutInputModel
|
||||
{
|
||||
public short AppId { get; set; }
|
||||
|
||||
public string WindowsUser { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string Action { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user