diff --git a/SharedLibrary/Commands/NativeCommands.cs b/SharedLibrary/Commands/NativeCommands.cs index 54a6ce480..5183a1049 100644 --- a/SharedLibrary/Commands/NativeCommands.cs +++ b/SharedLibrary/Commands/NativeCommands.cs @@ -975,6 +975,29 @@ namespace SharedLibrary.Commands } } + public class CSetPassword : Command + { + public CSetPassword() : base("setpassword", "set your authentication password", "sp", Player.Permission.Moderator, false, new CommandArgument[] + { + new CommandArgument() + { + Name = "password", + Required = true + } + }) + { } + + public override async Task ExecuteAsync(Event E) + { + string[] hashedPassword = Helpers.Hashing.Hash(E.Data); + + E.Origin.Password = hashedPassword[0]; + E.Origin.PasswordSalt = hashedPassword[1]; + + await E.Owner.Manager.GetClientService().Update(E.Origin); + } + } + public class CKillServer : Command { public CKillServer() : base("killserver", "kill the game server", "kill", Player.Permission.Administrator, false) diff --git a/SharedLibrary/Database/Models/EFClient.cs b/SharedLibrary/Database/Models/EFClient.cs index 82c120fae..52741d9a1 100644 --- a/SharedLibrary/Database/Models/EFClient.cs +++ b/SharedLibrary/Database/Models/EFClient.cs @@ -36,6 +36,9 @@ namespace SharedLibrary.Database.Models [ForeignKey("CurrentAliasId")] public virtual EFAlias CurrentAlias { get; set; } + public string Password { get; set; } + public string PasswordSalt { get; set; } + [NotMapped] public virtual string Name { diff --git a/SharedLibrary/Helpers/Hashing.cs b/SharedLibrary/Helpers/Hashing.cs new file mode 100644 index 000000000..cc6dbe3d8 --- /dev/null +++ b/SharedLibrary/Helpers/Hashing.cs @@ -0,0 +1,77 @@ +using System; +using SimpleCrypto; + +namespace SharedLibrary.Helpers +{ + public class Hashing + { + /// + /// Generate password hash and salt + /// + /// plaintext password + /// + public static string[] Hash(string password, string saltStr = null) + { + + string hash; + string salt; + var CryptoSvc = new PBKDF2(); + + // generate new hash + if (saltStr == null) + { + hash = CryptoSvc.Compute(password); + salt = CryptoSvc.Salt; + return new string[] + { + hash, + salt + }; + } + + else + { + hash = CryptoSvc.Compute(password, saltStr); + return new string[] + { + hash, + "" + }; + } + + + + /*//https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing + + byte[] salt; + + if (saltStr == null) + { + salt = new byte[128 / 8]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + } + + else + { + salt = Convert.FromBase64String(saltStr); + } + + // derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations) + string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA1, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + + return new string[] + { + hashed, + Convert.ToBase64String(salt) + };*/ + } + } +} diff --git a/SharedLibrary/RCon/Connection.cs b/SharedLibrary/RCon/Connection.cs index d60e38dcd..54e9fc9b1 100644 --- a/SharedLibrary/RCon/Connection.cs +++ b/SharedLibrary/RCon/Connection.cs @@ -11,10 +11,24 @@ namespace SharedLibrary.RCon { class ConnectionState { - public Socket Client { get; set; } - public const int BufferSize = 8192; - public byte[] Buffer = new byte[BufferSize]; - public readonly StringBuilder ResponseString = new StringBuilder(); + public Socket Client { get; private set; } + public int BufferSize { get; private set; } + public byte[] Buffer { get; private set; } + + private readonly StringBuilder sb; + + public StringBuilder ResponseString + { + get => sb; + } + + public ConnectionState(Socket cl) + { + BufferSize = 8192; + Buffer = new byte[BufferSize]; + Client = cl; + sb = new StringBuilder(); + } } public class Connection @@ -26,6 +40,7 @@ namespace SharedLibrary.RCon int FailedSends; int FailedReceives; DateTime LastQuery; + string response; ManualResetEvent OnConnected; ManualResetEvent OnSent; @@ -92,14 +107,11 @@ namespace SharedLibrary.RCon #if DEBUG Log.WriteDebug($"Sent {sentByteNum} bytes to {ServerConnection.RemoteEndPoint}"); #endif - FailedSends = 0; OnSent.Set(); } - catch (Exception e) + catch (SocketException) { - FailedSends++; - Log.WriteWarning($"Could not send RCon data to server - {e.Message}"); } } @@ -126,31 +138,33 @@ namespace SharedLibrary.RCon } else { - FailedReceives = 0; + response = connectionState.ResponseString.ToString(); OnReceived.Set(); } } else { + response = connectionState.ResponseString.ToString(); OnReceived.Set(); } } - catch (Exception) + catch (SocketException) { - FailedReceives++; + } } public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") { // will this really prevent flooding? - if ((DateTime.Now - LastQuery).TotalMilliseconds < 300) + if ((DateTime.Now - LastQuery).TotalMilliseconds < 150) { - await Task.Delay(300); - LastQuery = DateTime.Now; + await Task.Delay(150); } + LastQuery = DateTime.Now; + OnSent.Reset(); OnReceived.Reset(); string queryString = ""; @@ -168,66 +182,103 @@ namespace SharedLibrary.RCon byte[] payload = Encoding.Default.GetBytes(queryString); + retrySend: try { - retrySend: ServerConnection.BeginSend(payload, 0, payload.Length, 0, new AsyncCallback(OnSentCallback), ServerConnection); bool success = await Task.FromResult(OnSent.WaitOne(StaticHelpers.SocketTimeout)); if (!success) { + FailedSends++; #if DEBUG Log.WriteDebug($"{FailedSends} failed sends to {ServerConnection.RemoteEndPoint.ToString()}"); #endif if (FailedSends < 4) goto retrySend; - else - throw new NetworkException($"Could not send data to server - {new SocketException((int)SocketError.TimedOut).Message}"); + else if (FailedSends == 4) + Log.WriteError($"Failed to send data to {ServerConnection.RemoteEndPoint}"); + } + + else + { + if (FailedSends >= 4) + { + Log.WriteVerbose($"Resumed send RCon connection with {ServerConnection.RemoteEndPoint}"); + FailedSends = 0; + } } } catch (SocketException e) { // this result is normal if the server is not listening - if (e.HResult != (int)SocketError.ConnectionReset) + if (e.NativeErrorCode != (int)SocketError.ConnectionReset && + e.NativeErrorCode != (int)SocketError.TimedOut) throw new NetworkException($"Unexpected error while sending data to server - {e.Message}"); } - var connectionState = new ConnectionState - { - Client = ServerConnection - }; + var connectionState = new ConnectionState(ServerConnection); + retryReceive: try { - retryReceive: ServerConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, new AsyncCallback(OnReceivedCallback), connectionState); bool success = await Task.FromResult(OnReceived.WaitOne(StaticHelpers.SocketTimeout)); if (!success) { + FailedReceives++; #if DEBUG Log.WriteDebug($"{FailedReceives} failed receives from {ServerConnection.RemoteEndPoint.ToString()}"); #endif - FailedReceives++; if (FailedReceives < 4) - goto retryReceive; + goto retrySend; else if (FailedReceives == 4) - Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint}"); - else - throw new NetworkException($"Could not receive data from the server - {new SocketException((int)SocketError.TimedOut).Message}"); + { + Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint} after {FailedReceives} tries"); + } + + if (FailedReceives >= 4) + { + throw new NetworkException($"Could not receive data from the {ServerConnection.RemoteEndPoint}"); + } + } + + else + { + if (FailedReceives >= 4) + { + Log.WriteVerbose($"Resumed receive RCon connection from {ServerConnection.RemoteEndPoint}"); + FailedReceives = 0; + } } } catch (SocketException e) { // this result is normal if the server is not listening - if (e.HResult != (int)SocketError.ConnectionReset) + if (e.NativeErrorCode != (int)SocketError.ConnectionReset && + e.NativeErrorCode != (int)SocketError.TimedOut) throw new NetworkException($"Unexpected error while receiving data from server - {e.Message}"); + else if (FailedReceives < 4) + { + goto retryReceive; + } + + else if (FailedReceives == 4) + { + Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint} after {FailedReceives} tries"); + } + + if (FailedReceives >= 4) + { + throw new NetworkException(e.Message); + } } - string queryResponse = connectionState.ResponseString.ToString(); + string queryResponse = response; if (queryResponse.Contains("Invalid password")) throw new NetworkException("RCON password is invalid"); diff --git a/SharedLibrary/Services/ClientService.cs b/SharedLibrary/Services/ClientService.cs index 75296e56a..64ba40a0e 100644 --- a/SharedLibrary/Services/ClientService.cs +++ b/SharedLibrary/Services/ClientService.cs @@ -65,7 +65,7 @@ namespace SharedLibrary.Services Masked = false, NetworkId = entity.NetworkId, AliasLink = aliasLink, - CurrentAlias = existingAlias + CurrentAlias = existingAlias, }; context.Clients.Add(client); @@ -179,7 +179,9 @@ namespace SharedLibrary.Services client.Connections = entity.Connections; client.FirstConnection = entity.FirstConnection; client.Masked = entity.Masked; - client.TotalConnectionTime = entity.TotalConnectionTime; + client.TotalConnectionTime = entity.TotalConnectionTime; + client.Password = entity.Password; + client.PasswordSalt = entity.PasswordSalt; // update in database await context.SaveChangesAsync(); diff --git a/SharedLibrary/SharedLibrary.csproj b/SharedLibrary/SharedLibrary.csproj index c0cecb071..e815d056d 100644 --- a/SharedLibrary/SharedLibrary.csproj +++ b/SharedLibrary/SharedLibrary.csproj @@ -173,6 +173,7 @@ + @@ -242,6 +243,9 @@ 11.0.1 + + 0.3.30.26 + diff --git a/WebfrontCore/Application/Manager.cs b/WebfrontCore/Application/Manager.cs index c3238d14e..1a069b9d9 100644 --- a/WebfrontCore/Application/Manager.cs +++ b/WebfrontCore/Application/Manager.cs @@ -78,16 +78,26 @@ namespace IW4MAdmin { #region DATABASE var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted)) - .Select(c => new { c.IPAddress, c.ClientId, c.Level }); + .Select(c => new + { + c.Password, + c.PasswordSalt, + c.ClientId, + c.Level, + c.Name + }); foreach (var a in ipList) { try { - PrivilegedClients.Add(a.IPAddress, new Player() + PrivilegedClients.Add(a.ClientId, new Player() { + Name = a.Name, ClientId = a.ClientId, - Level = a.Level + Level = a.Level, + PasswordSalt = a.PasswordSalt, + Password = a.Password }); } @@ -213,6 +223,7 @@ namespace IW4MAdmin Commands.Add(new CMask()); Commands.Add(new CPruneAdmins()); Commands.Add(new CKillServer()); + Commands.Add(new CSetPassword()); foreach (Command C in SharedLibrary.Plugins.PluginImporter.ActiveCommands) Commands.Add(C); diff --git a/WebfrontCore/Application/Server.cs b/WebfrontCore/Application/Server.cs index 72d59b683..d8fe6b75c 100644 --- a/WebfrontCore/Application/Server.cs +++ b/WebfrontCore/Application/Server.cs @@ -791,16 +791,26 @@ namespace IW4MAdmin ((ApplicationManager)(Manager)).PrivilegedClients = new Dictionary(); var ClientSvc = new ClientService(); var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted)) - .Select(c => new { c.IPAddress, c.ClientId, c.Level }); + .Select(c => new + { + c.Password, + c.PasswordSalt, + c.ClientId, + c.Level, + c.Name + }); foreach (var a in ipList) { try { - ((ApplicationManager)(Manager)).PrivilegedClients.Add(a.IPAddress, new Player() + ((ApplicationManager)(Manager)).PrivilegedClients.Add(a.ClientId, new Player() { + Name = a.Name, ClientId = a.ClientId, - Level = a.Level + Level = a.Level, + PasswordSalt = a.PasswordSalt, + Password = a.Password }); } diff --git a/WebfrontCore/Controllers/AccountController.cs b/WebfrontCore/Controllers/AccountController.cs new file mode 100644 index 000000000..f10bb7229 --- /dev/null +++ b/WebfrontCore/Controllers/AccountController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using System.Security.Claims; + +namespace WebfrontCore.Controllers +{ + public class AccountController : BaseController + { + [HttpGet] + public async Task Login(int userId, string password) + { + if (userId == 0 || string.IsNullOrEmpty(password)) + { + return Unauthorized(); + } + + var client = IW4MAdmin.Program.ServerManager.PrivilegedClients[userId]; + string[] hashedPassword = await Task.FromResult(SharedLibrary.Helpers.Hashing.Hash(password, client.PasswordSalt)); + + if (hashedPassword[0] == client.Password) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, client.Name), + new Claim(ClaimTypes.Role, client.Level.ToString()), + new Claim(ClaimTypes.Sid, client.ClientId.ToString()) + }; + + var claimsIdentity = new ClaimsIdentity(claims, "login"); + var claimsPrinciple = new ClaimsPrincipal(claimsIdentity); + await HttpContext.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrinciple); + + return Ok(); + } + + return Unauthorized(); + } + } +} diff --git a/WebfrontCore/Controllers/ActionController.cs b/WebfrontCore/Controllers/ActionController.cs index 9a9dfd263..ba39b850c 100644 --- a/WebfrontCore/Controllers/ActionController.cs +++ b/WebfrontCore/Controllers/ActionController.cs @@ -71,5 +71,34 @@ namespace WebfrontCore.Controllers command = $"!unban @{targetId} {Reason}" })); } + + public IActionResult LoginForm() + { + var login = new ActionInfo() + { + ActionButtonLabel = "Login", + Name = "Login", + Inputs = new List() + { + new InputInfo() + { + Name = "UserID" + }, + new InputInfo() + { + Name = "Password", + Type = "password", + } + }, + Action = "Login" + }; + + return View("_ActionForm", login); + } + + public IActionResult Login(int userId, string password) + { + return RedirectToAction("Login", "Account", new { userId, password }); + } } } diff --git a/WebfrontCore/Controllers/BaseController.cs b/WebfrontCore/Controllers/BaseController.cs index 38a354ba2..dfe03123f 100644 --- a/WebfrontCore/Controllers/BaseController.cs +++ b/WebfrontCore/Controllers/BaseController.cs @@ -3,7 +3,11 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using SharedLibrary; using SharedLibrary.Database.Models; +using SharedLibrary.Objects; +using System; using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; namespace WebfrontCore.Controllers { @@ -24,18 +28,17 @@ namespace WebfrontCore.Controllers try { - var client = Manager.PrivilegedClients[context.HttpContext.Connection.RemoteIpAddress.ToString().ConvertToIP()]; - User.ClientId = client.ClientId; - User.Level = client.Level; + User.ClientId = Convert.ToInt32(base.User.Claims.First(c => c.Type == ClaimTypes.Sid).Value); + User.Level = (Player.Permission)Enum.Parse(typeof(Player.Permission), base.User.Claims.First(c => c.Type == ClaimTypes.Role).Value); + User.CurrentAlias = new EFAlias() { Name = base.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value }; } - catch (KeyNotFoundException) + catch (InvalidOperationException) { } - Authorized = context.HttpContext.Connection.RemoteIpAddress.ToString() == "127.0.0.1" || - User.ClientId >= 0; + Authorized = User.ClientId >= 0; ViewBag.Authorized = Authorized; ViewBag.Url = Startup.Configuration["Web:Address"]; ViewBag.User = User; diff --git a/WebfrontCore/Controllers/ClientController.cs b/WebfrontCore/Controllers/ClientController.cs index 6ec0390f7..2fe00abd6 100644 --- a/WebfrontCore/Controllers/ClientController.cs +++ b/WebfrontCore/Controllers/ClientController.cs @@ -63,7 +63,7 @@ namespace WebfrontCore.Controllers .Select(a => new ProfileMeta() { Key = "AliasEvent", - Value = $"Connected with name {a.Name}", + Value = $"Joined with alias {a.Name}", Sensitive = true, When = a.DateAdded })); diff --git a/WebfrontCore/Controllers/ConsoleController.cs b/WebfrontCore/Controllers/ConsoleController.cs index 7fc624917..c727916a9 100644 --- a/WebfrontCore/Controllers/ConsoleController.cs +++ b/WebfrontCore/Controllers/ConsoleController.cs @@ -28,29 +28,16 @@ namespace WebfrontCore.Controllers public async Task ExecuteAsync(int serverId, string command) { - var requestIPAddress = Request.HttpContext.Connection.RemoteIpAddress; - var intIP = requestIPAddress.ToString().ConvertToIP(); - -#if !DEBUG - var origin = (await IW4MAdmin.ApplicationManager.GetInstance().GetClientService().GetClientByIP(intIP)) - .OrderByDescending(c => c.Level) - .FirstOrDefault()?.AsPlayer() ?? new Player() - { - Name = "WebConsoleUser", - Level = Player.Permission.User, - IPAddress = intIP - }; -#else - var origin = (await Manager.GetClientService().GetUnique(0)).AsPlayer(); -#endif var server = Manager.Servers.First(s => s.GetHashCode() == serverId); - origin.CurrentServer = server; - var remoteEvent = new Event(Event.GType.Say, command, origin, null, server); + var client = User.AsPlayer(); + client.CurrentServer = server; + + var remoteEvent = new Event(Event.GType.Say, command, client, null, server); await server.ExecuteEvent(remoteEvent); - var response = server.CommandResult.Where(c => c.ClientId == origin.ClientId).ToList(); + var response = server.CommandResult.Where(c => c.ClientId == client.ClientId).ToList(); // remove the added command response for (int i = 0; i < response.Count; i++) diff --git a/WebfrontCore/Controllers/HomeController.cs b/WebfrontCore/Controllers/HomeController.cs index 601b5feec..b08e7a6ab 100644 --- a/WebfrontCore/Controllers/HomeController.cs +++ b/WebfrontCore/Controllers/HomeController.cs @@ -21,7 +21,7 @@ namespace WebfrontCore.Controllers public IActionResult Error() { - ViewBag.Description = "IW4MAdmin encountered and error"; + ViewBag.Description = "IW4MAdmin encountered an error"; ViewBag.Title = "Error!"; return View(); } diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index ef89f324b..e2907dba9 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using SharedLibrary.Configuration; namespace WebfrontCore { @@ -52,12 +49,23 @@ namespace WebfrontCore app.UseStaticFiles(); + app.UseCookieAuthentication(new CookieAuthenticationOptions() + { + AccessDeniedPath = "/Account/Login/", + AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme, + AutomaticAuthenticate = true, + AutomaticChallenge = true, + LoginPath = "/Account/Login/" + }); + app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); + + //app.UseBasicAuthentication(Authentication.Basic.Generate()); } } } diff --git a/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs b/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs index fb17b3104..4411bc687 100644 --- a/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs +++ b/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs @@ -19,8 +19,8 @@ namespace WebfrontCore.ViewComponents try { - var a = IW4MAdmin.ApplicationManager.GetInstance() - .PrivilegedClients[HttpContext.Connection.RemoteIpAddress.ToString().ConvertToIP()]; + // var a = IW4MAdmin.ApplicationManager.GetInstance() + //.PrivilegedClients[HttpContext.Connection.RemoteIpAddress.ToString().ConvertToIP()]; } catch (KeyNotFoundException) diff --git a/WebfrontCore/Views/Action/_ActionForm.cshtml b/WebfrontCore/Views/Action/_ActionForm.cshtml index e0ef35a9c..83dafc70d 100644 --- a/WebfrontCore/Views/Action/_ActionForm.cshtml +++ b/WebfrontCore/Views/Action/_ActionForm.cshtml @@ -3,20 +3,21 @@ Layout = null; }
-
- @foreach (var input in Model.Inputs) - { + @foreach (var input in Model.Inputs) + { +
@input.Name
- { + @{ string inputType = input.Type ?? "text"; string value = input.Value ?? ""; } - } -
+ +
+ }
\ No newline at end of file diff --git a/WebfrontCore/Views/Client/Profile/Index.cshtml b/WebfrontCore/Views/Client/Profile/Index.cshtml index 070b75c51..de40b22bd 100644 --- a/WebfrontCore/Views/Client/Profile/Index.cshtml +++ b/WebfrontCore/Views/Client/Profile/Index.cshtml @@ -23,13 +23,13 @@ @if (Model.LevelInt < (int)ViewBag.User.Level && (SharedLibrary.Objects.Player.Permission)Model.LevelInt != SharedLibrary.Objects.Player.Permission.Banned) { - + } @if (Model.LevelInt < (int)ViewBag.User.Level && (SharedLibrary.Objects.Player.Permission)Model.LevelInt == SharedLibrary.Objects.Player.Permission.Banned) { - + } diff --git a/WebfrontCore/Views/Server/_ClientActivity.cshtml b/WebfrontCore/Views/Server/_ClientActivity.cshtml index 4c798c6a9..a0c3bf811 100644 --- a/WebfrontCore/Views/Server/_ClientActivity.cshtml +++ b/WebfrontCore/Views/Server/_ClientActivity.cshtml @@ -32,7 +32,7 @@ for (int i = 0; i < half; i++) { string levelColorClass = !ViewBag.Authorized ? "" : $"level-color-{Model.Players[i].Level.ToLower()}"; - @Html.ActionLink(Model.Players[i].Name, "ProfileAsync", "Client", new { id = Model.Players[i].ClientId }, new { @class=levelColorClass })
+ @Html.ActionLink(Model.Players[i].Name, "ProfileAsync", "Client", new { id = Model.Players[i].ClientId }, new { @class = levelColorClass })
} } @@ -46,4 +46,29 @@ } + +@if (Model.ChatHistory.Length > 0) +{ +
+} +
+ @{ + for (int i = 0; i < Model.ChatHistory.Length; i++) + { + string message = @Model.ChatHistory[i].Message; + if (Model.ChatHistory[i].Message == "CONNECTED") + { + @Model.ChatHistory[i].Name
+ } + if (Model.ChatHistory[i].Message == "DISCONNECTED") + { + + @Model.ChatHistory[i].Name
+ } + if (Model.ChatHistory[i].Message != "CONNECTED" && Model.ChatHistory[i].Message != "DISCONNECTED") + { + @Model.ChatHistory[i].Name — @message.Substring(0, Math.Min(65, message.Length))
+ } + } + }
\ No newline at end of file diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index a4662b013..2a56968f8 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -38,6 +38,15 @@ { } + @if (ViewBag.Authorized) + { + + } + else + { + + + }
@@ -62,7 +71,6 @@ -