diff --git a/RustRconServerManager.Backend/Cli/ResetPasswordCli.cs b/RustRconServerManager.Backend/Cli/ResetPasswordCli.cs index 903c184..0728bde 100644 --- a/RustRconServerManager.Backend/Cli/ResetPasswordCli.cs +++ b/RustRconServerManager.Backend/Cli/ResetPasswordCli.cs @@ -22,18 +22,18 @@ public static async Task RunAsync(IServiceProvider services) Console.WriteLine("RustRconServerManager - Password Reset"); Console.WriteLine(); - Console.Write("Account email: "); - var email = Console.ReadLine()?.Trim(); - if (string.IsNullOrWhiteSpace(email)) + Console.Write("Account username: "); + var username = Console.ReadLine()?.Trim(); + if (string.IsNullOrWhiteSpace(username)) { - Console.WriteLine("No email entered. Aborting."); + Console.WriteLine("No username entered. Aborting."); return 1; } - var user = await userManager.FindByEmailAsync(email); + var user = await userManager.FindByNameAsync(username); if (user == null) { - Console.WriteLine($"No account found for '{email}'."); + Console.WriteLine($"No account found for '{username}'."); return 1; } @@ -99,7 +99,7 @@ public static async Task RunAsync(IServiceProvider services) } Console.WriteLine(); - Console.WriteLine($"Password for '{email}' has been reset. Any existing sessions have been signed out."); + Console.WriteLine($"Password for '{username}' has been reset. Any existing sessions have been signed out."); return 0; } diff --git a/RustRconServerManager.Backend/Controllers/AccountController.cs b/RustRconServerManager.Backend/Controllers/AccountController.cs index d67a0c8..0fea829 100644 --- a/RustRconServerManager.Backend/Controllers/AccountController.cs +++ b/RustRconServerManager.Backend/Controllers/AccountController.cs @@ -29,12 +29,12 @@ public AccountController(AppDbContext dbContext) [HttpGet("BasicInfo")] public IActionResult GetBasicInfo() { - var userEmail = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userEmail)) - return Unauthorized("No email claim found"); + if (string.IsNullOrEmpty(userId)) + return Unauthorized("No user claim found"); - var user = _dbContext.Users.Where(x => x.Email == userEmail).FirstOrDefault(); + var user = _dbContext.Users.Where(x => x.Id == userId).FirstOrDefault(); if (user == null) return NotFound(); @@ -42,7 +42,9 @@ public IActionResult GetBasicInfo() return Ok(new { Email = user.Email, + Username = user.UserName, DisplayName = user.DisplayName, + HasChosenUsername = user.HasChosenUsername, IsModerator = user.IsModerator, isAdmin = user.isAdmin }); @@ -54,12 +56,12 @@ public IActionResult GetBasicInfo() [HttpGet("SelectedServerId")] public IActionResult GetSelectedServerId() { - var userEmail = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userEmail)) - return Unauthorized("No email claim found"); + if (string.IsNullOrEmpty(userId)) + return Unauthorized("No user claim found"); - var user = _dbContext.Users.Where(x => x.Email == userEmail).FirstOrDefault(); + var user = _dbContext.Users.Where(x => x.Id == userId).FirstOrDefault(); if (user == null) return NotFound(); @@ -77,12 +79,12 @@ public IActionResult GetSelectedServerId() public IActionResult AccountInformation() { - var userEmail = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userEmail)) - return Unauthorized("No email claim found"); + if (string.IsNullOrEmpty(userId)) + return Unauthorized("No user claim found"); - var accountInformation = _dbContext.Users.Where(x => x.Email == userEmail).FirstOrDefault(); + var accountInformation = _dbContext.Users.Where(x => x.Id == userId).FirstOrDefault(); if (accountInformation == null) return NotFound(); @@ -94,6 +96,7 @@ public IActionResult AccountInformation() Account_AccountInformationDTO dto = new Account_AccountInformationDTO(); dto.Email = accountInformation.Email; + dto.Username = accountInformation.UserName; dto.DisplayName = accountInformation.DisplayName; dto.CreatedAt = accountInformation.CreatedAt; dto.isAdmin = accountInformation.isAdmin; @@ -119,12 +122,12 @@ public async Task SetDisplayName([FromBody] Account_SetDisplayNam if (trimmed.Length > 50) return BadRequest("Display name must be 50 characters or fewer."); - var userEmail = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userEmail)) - return Unauthorized("No email claim found"); + if (string.IsNullOrEmpty(userId)) + return Unauthorized("No user claim found"); - var user = await _dbContext.Users.FirstOrDefaultAsync(x => x.Email == userEmail); + var user = await _dbContext.Users.FirstOrDefaultAsync(x => x.Id == userId); if (user == null) return NotFound(); @@ -134,5 +137,45 @@ public async Task SetDisplayName([FromBody] Account_SetDisplayNam return Ok(new { DisplayName = user.DisplayName }); } + + /// + /// Sets the current user's username (used to log in instead of an email address). + /// Required for every account going forward - existing accounts created before + /// username-based login existed had it silently set equal to their email, and are + /// prompted once to pick a real one (see MainLayout's RequireUsernameGate). + /// + [HttpPut("Username")] + public async Task SetUsername([FromBody] Account_SetUsernameDTO dto) + { + var trimmed = dto.Username?.Trim() ?? string.Empty; + + if (string.IsNullOrEmpty(trimmed)) + return BadRequest("Username is required."); + + if (trimmed.Length > 50) + return BadRequest("Username must be 50 characters or fewer."); + + var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + return Unauthorized("No user claim found"); + + var user = await _dbContext.Users.FirstOrDefaultAsync(x => x.Id == userId); + + if (user == null) + return NotFound(); + + var existing = await _dbContext.Users + .FirstOrDefaultAsync(x => x.NormalizedUserName == trimmed.ToUpperInvariant() && x.Id != userId); + if (existing != null) + return BadRequest("That username is already taken."); + + user.UserName = trimmed; + user.NormalizedUserName = trimmed.ToUpperInvariant(); + user.HasChosenUsername = true; + await _dbContext.SaveChangesAsync(); + + return Ok(new { Username = user.UserName }); + } } } \ No newline at end of file diff --git a/RustRconServerManager.Backend/Controllers/AuthController.cs b/RustRconServerManager.Backend/Controllers/AuthController.cs index 332302f..0c511ad 100644 --- a/RustRconServerManager.Backend/Controllers/AuthController.cs +++ b/RustRconServerManager.Backend/Controllers/AuthController.cs @@ -78,6 +78,11 @@ public async Task Setup(Authorization_SetupRequestDTO model) return BadRequest(new { message = "Display name is required" }); } + if (string.IsNullOrWhiteSpace(model.Username)) + { + return BadRequest(new { message = "Username is required" }); + } + // Get or create default SystemProfile var systemProfile = await _dbContext.SystemProfiles.FirstOrDefaultAsync(); if (systemProfile == null) @@ -111,12 +116,13 @@ public async Task Setup(Authorization_SetupRequestDTO model) // Create the admin user var user = new ApplicationUser { - UserName = model.Email, - Email = model.Email, + UserName = model.Username.Trim(), + Email = string.IsNullOrWhiteSpace(model.Email) ? null : model.Email.Trim(), DisplayName = model.DisplayName.Trim(), SystemProfileId = systemProfile.Id, isAdmin = true, - EmailConfirmed = true, // Auto-confirm email for initial admin + EmailConfirmed = !string.IsNullOrWhiteSpace(model.Email), // Auto-confirm email for initial admin, if one was provided + HasChosenUsername = true, CreatedAt = DateTime.UtcNow }; @@ -229,12 +235,12 @@ public async Task Logout() { // Get the token hash from claims var tokenHash = User.FindFirst(System.Security.Claims.ClaimTypes.Hash)?.Value; - var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value; + var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(tokenHash) || string.IsNullOrEmpty(userEmail)) + if (string.IsNullOrEmpty(tokenHash) || string.IsNullOrEmpty(userId)) return BadRequest("Missing token information"); - var user = await _userManager.FindByEmailAsync(userEmail); + var user = await _userManager.FindByIdAsync(userId); if (user == null) return NotFound("User not found"); @@ -259,11 +265,11 @@ public async Task Logout() public async Task LogoutAll() { // Logout from all devices/browsers - var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value; - if (string.IsNullOrEmpty(userEmail)) + var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userId)) return BadRequest("Missing user information"); - var user = await _userManager.FindByEmailAsync(userEmail); + var user = await _userManager.FindByIdAsync(userId); if (user == null) return NotFound("User not found"); @@ -345,7 +351,7 @@ public IActionResult Me() [HttpPost("login")] public async Task Login(Authorization_UserLoginDTO model) { - var user = await _userManager.FindByEmailAsync(model.Email); + var user = await _userManager.FindByNameAsync(model.Username); if (user == null) return Unauthorized(new Authorization_LoginResponseDTO { @@ -623,9 +629,9 @@ private string GenerateJwtToken(ApplicationUser user, string sessionHash = "") var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var userAgent = Request.Headers["User-Agent"].ToString() ?? "unknown"; - var claims = new[] + var claims = new List { - new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Name, user.UserName ?? user.Id), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim("ip", ipAddress), new Claim("ua", userAgent), @@ -633,6 +639,13 @@ private string GenerateJwtToken(ApplicationUser user, string sessionHash = "") new Claim(ClaimTypes.Hash, sessionHash) }; + // Email is optional now - only include the claim when the account actually has one, + // since a Claim's value can't be null. + if (!string.IsNullOrEmpty(user.Email)) + { + claims.Add(new Claim(ClaimTypes.Email, user.Email)); + } + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); diff --git a/RustRconServerManager.Backend/Controllers/DashboardController.cs b/RustRconServerManager.Backend/Controllers/DashboardController.cs index 795d0b0..a070735 100644 --- a/RustRconServerManager.Backend/Controllers/DashboardController.cs +++ b/RustRconServerManager.Backend/Controllers/DashboardController.cs @@ -234,7 +234,7 @@ public async Task BanPlayer([FromBody] BanPlayerRequest request) DateTime? expiryDate = banExpiredAt == now ? null : banExpiredAt; // Get current user email for audit trail - var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "System"; + var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "System"; // Create PlayerBan record (single source of truth for bans) // If global ban, use ServerId = -1 @@ -445,7 +445,7 @@ public async Task UnbanPlayer([FromBody] UnbanPlayerRequest reque if (banRecord == null && globalBan == null) return NotFound("Ban record not found"); - var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "System"; + var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "System"; var now = DateTime.UtcNow; if (globalBan != null) @@ -873,7 +873,7 @@ public async Task ToggleGlobalBan(int banId, [FromBody] ToggleGlo _logger.LogInformation("[GLOBAL UNBAN] Toggling off global ban for {SteamId}. Unbanning from all servers...", ban.SteamId?.Replace("\r", "").Replace("\n", "")); // Get current user email for audit trail - var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "System"; + var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "System"; var now = DateTime.UtcNow; // Find all bans for this player with the note indicating they're from a global ban @@ -960,7 +960,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques } // Get current user email for audit trail - var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "System"; + var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "System"; var now = DateTime.UtcNow; // If this is a global ban, unban from all servers @@ -1100,7 +1100,7 @@ public async Task UpdateBan(int banId, [FromBody] UpdateBanReques return Unauthorized(); } - var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "System"; + var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "System"; var now = DateTime.UtcNow; // Calculate new expiry diff --git a/RustRconServerManager.Backend/Controllers/ModeratorController.cs b/RustRconServerManager.Backend/Controllers/ModeratorController.cs index 64a985f..b353278 100644 --- a/RustRconServerManager.Backend/Controllers/ModeratorController.cs +++ b/RustRconServerManager.Backend/Controllers/ModeratorController.cs @@ -116,11 +116,13 @@ public async Task Create([FromBody] Moderator_CreateDTO dto) var moderator = new ApplicationUser { UserName = dto.Username, - Email = dto.Email ?? $"{dto.Username}@moderator.local", + Email = string.IsNullOrWhiteSpace(dto.Email) ? null : dto.Email, + EmailConfirmed = !string.IsNullOrWhiteSpace(dto.Email), DisplayName = dto.DisplayName, SystemProfileId = profile.Id, IsModerator = true, isLoginBlocked = false, + HasChosenUsername = true, CreatedAt = DateTime.UtcNow }; diff --git a/RustRconServerManager.Backend/Controllers/SecurityController.cs b/RustRconServerManager.Backend/Controllers/SecurityController.cs index b877dc0..2bba55d 100644 --- a/RustRconServerManager.Backend/Controllers/SecurityController.cs +++ b/RustRconServerManager.Backend/Controllers/SecurityController.cs @@ -110,38 +110,15 @@ public async Task ChangeEmail([FromBody] Security_ChangeEmailDTO return BadRequest(string.Join(", ", setEmailResult.Errors.Select(e => e.Description))); } - await _userManager.SetUserNameAsync(user, dto.NewEmail); + // Username is a separate, independent identifier now (login is by username, + // not email) - it must NOT be overwritten here. user.EmailConfirmed = true; await _userManager.UpdateAsync(user); - // The current session's JWT still carries the OLD email claim, and - // SecurityBindingMiddleware re-resolves the user by that claim on every - // request (including the frontend's follow-up logout call and even loading - // the login page itself) - once the email no longer matches any user, that - // middleware would 401 all of those with a raw, unstyled error response - // instead of letting the app load. Revoke the session and clear the cookie - // here so the browser has no stale credential left to send. - var tokenHash = User.FindFirst(System.Security.Claims.ClaimTypes.Hash)?.Value; - if (!string.IsNullOrEmpty(tokenHash)) - { - var userSession = await _dbContext.UserSessions - .FirstOrDefaultAsync(s => s.UserId == user.Id && s.SessionHash == tokenHash); - if (userSession != null) - { - userSession.IsRevoked = true; - await _dbContext.SaveChangesAsync(); - } - } - - Response.Cookies.Delete("rrsm_auth", new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps, - SameSite = SameSiteMode.Strict, - Path = "/" - }); - - return Ok(new { message = "Email changed successfully. Please log in again with your new email address." }); + // Identity/session resolution is keyed on the user's Id (see GetUser(), + // SecurityBindingMiddleware), not email, so changing it doesn't invalidate the + // current session - no need to log the user out. + return Ok(new { message = "Email changed successfully." }); } catch (Exception ex) { diff --git a/RustRconServerManager.Backend/Extensions/ClaimsPrincipalExtensions.cs b/RustRconServerManager.Backend/Extensions/ClaimsPrincipalExtensions.cs index c46c41a..72e9026 100644 --- a/RustRconServerManager.Backend/Extensions/ClaimsPrincipalExtensions.cs +++ b/RustRconServerManager.Backend/Extensions/ClaimsPrincipalExtensions.cs @@ -19,13 +19,13 @@ public static async Task GetUserSystemProfile(this ClaimsPrincipa public static async Task GetUser(this ClaimsPrincipal user, AppDbContext context) { - var userEmail = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + var userId = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; var sessionHash = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Hash)?.Value; - if (string.IsNullOrWhiteSpace(userEmail) || string.IsNullOrWhiteSpace(sessionHash)) + if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(sessionHash)) throw new UserNotAuthenticatedException(); - return await context.Users.FirstOrDefaultAsync(u => u.Email == userEmail) + return await context.Users.FirstOrDefaultAsync(u => u.Id == userId) ?? throw new UserNotAuthenticatedException(); } diff --git a/RustRconServerManager.Backend/Middleware/SecurityBindingMiddleware.cs b/RustRconServerManager.Backend/Middleware/SecurityBindingMiddleware.cs index 20dccb5..e444fd7 100644 --- a/RustRconServerManager.Backend/Middleware/SecurityBindingMiddleware.cs +++ b/RustRconServerManager.Backend/Middleware/SecurityBindingMiddleware.cs @@ -44,17 +44,17 @@ public async Task Invoke(HttpContext context) return; } - var userEmail = user.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email)?.Value; + var userId = user.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; var tokenHash = user.FindFirst(System.Security.Claims.ClaimTypes.Hash)?.Value; - if (string.IsNullOrEmpty(userEmail) || string.IsNullOrEmpty(tokenHash)) + if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(tokenHash)) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; await context.Response.WriteAsync("Unauthorized: Missing claims."); return; } - var dbUser = await _dbContext.Users.FirstOrDefaultAsync(u => u.Email == userEmail); + var dbUser = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); if (dbUser == null) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; diff --git a/RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.Designer.cs b/RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.Designer.cs new file mode 100644 index 0000000..1058343 --- /dev/null +++ b/RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.Designer.cs @@ -0,0 +1,2287 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RustRconServerManager.Backend.Database; + +#nullable disable + +namespace RustRconServerManager.Backend.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260819151536_AddHasChosenUsernameToApplicationUser")] + partial class AddHasChosenUsernameToApplicationUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.AggregatedStat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregationType") + .HasColumnType("int"); + + b.Property("Avg") + .HasColumnType("double"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Max") + .HasColumnType("double"); + + b.Property("Min") + .HasColumnType("double"); + + b.Property("SampleCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Stat") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId", "Stat", "Timestamp", "AggregationType"); + + b.ToTable("AggregatedStats"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DiscordId") + .HasColumnType("longtext"); + + b.Property("DisplayName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("HasChosenUsername") + .HasColumnType("tinyint(1)"); + + b.Property("IsModerator") + .HasColumnType("tinyint(1)"); + + b.Property("LastLoginAt") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PasswordResetCode") + .HasColumnType("longtext"); + + b.Property("PasswordResetCodeExpiry") + .HasColumnType("datetime(6)"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("SelectedServerId") + .HasColumnType("int"); + + b.Property("SessionHash") + .HasColumnType("longtext"); + + b.Property("SteamId") + .HasColumnType("longtext"); + + b.Property("SystemProfileId") + .HasColumnType("int"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Website") + .HasColumnType("longtext"); + + b.Property("isAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("isLoginBlocked") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("SystemProfileId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("UserEmail") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("longtext"); + + b.Property("IsFlagged") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("PlayerName") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("SteamId") + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.DeveloperVacBanOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DaysSinceLastBan") + .HasColumnType("int"); + + b.Property("NumberOfVACBans") + .HasColumnType("int"); + + b.Property("SteamId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("VACBanned") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("SteamId") + .IsUnique(); + + b.ToTable("DeveloperVacBanOverrides"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.DevicePushToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastUsedAt") + .HasColumnType("datetime(6)"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Token") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("DevicePushTokens"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.IpVpnCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CheckedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("IsVpn") + .HasColumnType("tinyint(1)"); + + b.Property("Provider") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ProxyType") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("IpAddress") + .IsUnique(); + + b.ToTable("IpVpnCache"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.LegalConsent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AcceptedAt") + .HasColumnType("datetime(6)"); + + b.Property("AcceptedPrivacyPolicy") + .HasColumnType("tinyint(1)"); + + b.Property("AcceptedTermsAndConditions") + .HasColumnType("tinyint(1)"); + + b.Property("ConsentAnonymousMetrics") + .HasColumnType("tinyint(1)"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("PrivacyVersion") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TermsVersion") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("LegalConsents"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.MapData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImageData") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImagePath") + .HasColumnType("longtext"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("MapSeed") + .HasColumnType("int"); + + b.Property("MapSize") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId") + .IsUnique(); + + b.ToTable("MapData"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ModeratorPagePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("GrantedAt") + .HasColumnType("datetime(6)"); + + b.Property("PageRoute") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "PageRoute") + .IsUnique(); + + b.ToTable("ModeratorPagePermissions"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ModeratorServerPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("GrantedAt") + .HasColumnType("datetime(6)"); + + b.Property("RconServerId") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RconServerId"); + + b.HasIndex("UserId", "RconServerId") + .IsUnique(); + + b.ToTable("ModeratorServerPermissions"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PanelSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AnalyticsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdateEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeveloperModeEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastAnalyticsSentAt") + .HasColumnType("datetime(6)"); + + b.Property("MinimumLogLevel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SteamApiKeyEncrypted") + .HasColumnType("longtext"); + + b.Property("SystemProfileId") + .HasColumnType("int"); + + b.Property("TimezoneId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SystemProfileId") + .IsUnique(); + + b.ToTable("PanelSettings"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PlayerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Expiry") + .HasColumnType("bigint"); + + b.Property("Group") + .HasColumnType("longtext"); + + b.Property("InternalNote") + .HasColumnType("longtext"); + + b.Property("IsGlobalBan") + .HasColumnType("tinyint(1)"); + + b.Property("IsLifted") + .HasColumnType("tinyint(1)"); + + b.Property("LiftedAt") + .HasColumnType("datetime(6)"); + + b.Property("LiftedBy") + .HasColumnType("longtext"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("SteamId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlayerBans"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PlayerBanHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BannedAt") + .HasColumnType("datetime(6)"); + + b.Property("BannedBy") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DurationHours") + .HasColumnType("bigint"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalBan") + .HasColumnType("tinyint(1)"); + + b.Property("LiftReason") + .HasColumnType("longtext"); + + b.Property("LiftedAt") + .HasColumnType("datetime(6)"); + + b.Property("LiftedBy") + .HasColumnType("longtext"); + + b.Property("PlayerBanId") + .HasColumnType("int"); + + b.Property("PlayerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("SteamId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("PlayerBanHistories"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PlayerIpHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FirstUsed") + .HasColumnType("datetime(6)"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("IsVpn") + .HasColumnType("tinyint(1)"); + + b.Property("LastUsed") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("SteamId") + .IsRequired() + .HasMaxLength(17) + .HasColumnType("varchar(17)"); + + b.Property("TimesUsed") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SteamId", "ServerId"); + + b.HasIndex("SteamId", "ServerId", "IpAddress") + .IsUnique(); + + b.ToTable("PlayerIpHistory"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PlayerKillLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsPVP") + .HasColumnType("tinyint(1)"); + + b.Property("KilledById") + .HasColumnType("longtext"); + + b.Property("KilledByName") + .HasColumnType("longtext"); + + b.Property("KilledPlayerId") + .HasColumnType("longtext"); + + b.Property("KilledPlayerName") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PlayerKillLogs"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PlayerNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("varchar(450)"); + + b.Property("Note") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("SteamId") + .IsRequired() + .HasMaxLength(17) + .HasColumnType("varchar(17)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("PlayerNotes"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PlayerReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdminNotes") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("ReportedId") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ReportedName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ReporterId") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ReporterName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("varchar(255)"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ReviewedByUserId"); + + b.ToTable("PlayerReports"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PluginVersionCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CachedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("LatestVersion") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PluginName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PluginUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Source") + .HasColumnType("int"); + + b.Property("UmodRateLimitRemaining") + .HasColumnType("int"); + + b.Property("UmodRateLimitTotal") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("PluginName"); + + b.ToTable("PluginVersionCache"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PresetCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Command") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsGlobal") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RconServerId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RconServerId"); + + b.ToTable("PresetCommands"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.RconLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RconLogEntries"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.RconServer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("EncryptedHost") + .HasColumnType("longtext"); + + b.Property("EncryptedPassword") + .HasColumnType("longtext"); + + b.Property("EnvironmentSecret") + .HasColumnType("longtext"); + + b.Property("GamePort") + .HasColumnType("int"); + + b.Property("LastSeen") + .HasColumnType("datetime(6)"); + + b.Property("LatestEntityCount") + .HasColumnType("int"); + + b.Property("LatestFpsCount") + .HasColumnType("int"); + + b.Property("LatestJoiningPlayers") + .HasColumnType("int"); + + b.Property("LatestMap") + .HasColumnType("longtext"); + + b.Property("LatestMemoryUsage") + .HasColumnType("int"); + + b.Property("LatestPlayerCount") + .HasColumnType("int"); + + b.Property("LatestQueuedPlayers") + .HasColumnType("int"); + + b.Property("LatestServerProtocol") + .HasColumnType("longtext"); + + b.Property("LatestServerVersion") + .HasColumnType("int"); + + b.Property("LatestUptime") + .HasColumnType("int"); + + b.Property("ModFramework") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("QueryPort") + .HasColumnType("int"); + + b.Property("RconPort") + .HasColumnType("int"); + + b.Property("RrsmModInitialized") + .HasColumnType("tinyint(1)"); + + b.Property("RustRconServerManagerModInstalled") + .HasColumnType("tinyint(1)"); + + b.Property("ServerHeaderImageData") + .HasColumnType("longblob"); + + b.Property("ServerHostname") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerLogoImageData") + .HasColumnType("longblob"); + + b.Property("SystemProfileId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SystemProfileId"); + + b.ToTable("RconServers"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.RustItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StackSize") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RustItems"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ScheduledCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Command") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfMonth") + .HasColumnType("int"); + + b.Property("DaysOfWeek") + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ExecuteAt") + .HasColumnType("datetime(6)"); + + b.Property("ExecutionCount") + .HasColumnType("int"); + + b.Property("ExecutionHour") + .HasColumnType("int"); + + b.Property("ExecutionMinute") + .HasColumnType("int"); + + b.Property("IntervalHours") + .HasColumnType("int"); + + b.Property("IntervalMinutes") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastExecutedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastExecutionError") + .HasColumnType("longtext"); + + b.Property("LastExecutionSuccess") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NextExecutionAt") + .HasColumnType("datetime(6)"); + + b.Property("RconServerId") + .HasColumnType("int"); + + b.Property("ScheduleType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UtcOffsetMinutes") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RconServerId"); + + b.ToTable("ScheduledCommands"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ServerPluginSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PluginName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RustServerId") + .HasColumnType("int"); + + b.Property("Source") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RustServerId", "PluginName") + .IsUnique(); + + b.ToTable("ServerPluginSources"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ServerProtectionSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BanDurationMinutes") + .HasColumnType("int"); + + b.Property("BlockPrivateSteamProfiles") + .HasColumnType("tinyint(1)"); + + b.Property("CountryFilterMode") + .HasColumnType("int"); + + b.Property("CountryList") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnablePublicBanProtection") + .HasColumnType("tinyint(1)"); + + b.Property("EnableVacBanProtection") + .HasColumnType("tinyint(1)"); + + b.Property("EnableVpnCheck") + .HasColumnType("tinyint(1)"); + + b.Property("EnableVpnProtection") + .HasColumnType("tinyint(1)"); + + b.Property("EnableWhitelistOnly") + .HasColumnType("tinyint(1)"); + + b.Property("MaxPublicBans") + .HasColumnType("int"); + + b.Property("MaxVacBans") + .HasColumnType("int"); + + b.Property("MinDaysSinceLastVACBan") + .HasColumnType("int"); + + b.Property("PrivateSteamProfileAction") + .HasColumnType("int"); + + b.Property("PublicBanAction") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VacBanAction") + .HasColumnType("int"); + + b.Property("VpnProtectionAction") + .HasColumnType("int"); + + b.Property("WhitelistOnlyKickMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WhitelistedSteamIds") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerProtectionSettings"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ServerWebhookSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnablePlayerBanWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnablePlayerConnectWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnablePlayerDisconnectWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnablePlayerKickWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnablePlayerKillWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnablePlayerReportWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnableServerOfflineWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnableServerOnlineWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("EnableServerProtectionWebhook") + .HasColumnType("tinyint(1)"); + + b.Property("PlayerBanCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerBanFormat") + .HasColumnType("int"); + + b.Property("PlayerBanTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerBanWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerConnectCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerConnectFormat") + .HasColumnType("int"); + + b.Property("PlayerConnectTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerConnectWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerDisconnectCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerDisconnectFormat") + .HasColumnType("int"); + + b.Property("PlayerDisconnectTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerDisconnectWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerKickCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerKickFormat") + .HasColumnType("int"); + + b.Property("PlayerKickTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerKickWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerKillCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerKillFormat") + .HasColumnType("int"); + + b.Property("PlayerKillTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerKillWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerReportCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerReportFormat") + .HasColumnType("int"); + + b.Property("PlayerReportTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlayerReportWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("ServerOfflineCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerOfflineFormat") + .HasColumnType("int"); + + b.Property("ServerOfflineTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerOfflineWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerOnlineCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerOnlineFormat") + .HasColumnType("int"); + + b.Property("ServerOnlineTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerOnlineWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerProtectionCustomContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerProtectionFormat") + .HasColumnType("int"); + + b.Property("ServerProtectionTextContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerProtectionWebhookUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerWebhookSettings"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.SleepingBagData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerSteamId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("SleepingBags"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.StatsHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Stat") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId", "Stat", "CreatedAt"); + + b.ToTable("StatsHistories"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.SteamPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("AvatarLastUpdated") + .HasColumnType("datetime(6)"); + + b.Property("Country") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DaysSinceLastVACBan") + .HasColumnType("int"); + + b.Property("FirstSeen") + .HasColumnType("datetime(6)"); + + b.Property("IsOnline") + .HasColumnType("tinyint(1)"); + + b.Property("LastIp") + .HasColumnType("longtext"); + + b.Property("LastSeen") + .HasColumnType("datetime(6)"); + + b.Property("LatestHealth") + .HasColumnType("float"); + + b.Property("LatestPing") + .HasColumnType("int"); + + b.Property("LatestPositionX") + .HasColumnType("float"); + + b.Property("LatestPositionY") + .HasColumnType("float"); + + b.Property("LatestPositionZ") + .HasColumnType("float"); + + b.Property("LatestTeamId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("NumberOfVACBans") + .HasColumnType("int"); + + b.Property("ProfileVisibility") + .HasColumnType("int"); + + b.Property("RustPlaytimeMinutes") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("SteamAccountCreated") + .HasColumnType("datetime(6)"); + + b.Property("SteamId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VACBanned") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("SteamPlayers"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.SystemProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Secret") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("SystemProfiles"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ToolCupboardData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthorizedPlayers") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerSteamId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("ToolCupboards"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ActionValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ChatConditionType") + .HasColumnType("longtext"); + + b.Property("ChatConditionValue") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DelaySeconds") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastExecutionError") + .HasColumnType("longtext"); + + b.Property("LastExecutionSuccess") + .HasColumnType("tinyint(1)"); + + b.Property("LastTriggeredAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RconServerId") + .HasColumnType("int"); + + b.Property("TriggerCount") + .HasColumnType("int"); + + b.Property("TriggerEvent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WebhookUrl") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("RconServerId"); + + b.ToTable("Triggers"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.UserNotificationPreference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("PlayerBanned") + .HasColumnType("tinyint(1)"); + + b.Property("PlayerOffline") + .HasColumnType("tinyint(1)"); + + b.Property("PlayerOnline") + .HasColumnType("tinyint(1)"); + + b.Property("PlayerReported") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("ServerOffline") + .HasColumnType("tinyint(1)"); + + b.Property("ServerOnline") + .HasColumnType("tinyint(1)"); + + b.Property("ServerProtection") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("UserId", "ServerId") + .IsUnique(); + + b.ToTable("UserNotificationPreferences"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("LastActivityAt") + .HasColumnType("datetime(6)"); + + b.Property("SessionHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserAgent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ApplicationUser", b => + { + b.HasOne("RustRconServerManager.Backend.Models.SystemProfile", "SystemProfile") + .WithMany() + .HasForeignKey("SystemProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SystemProfile"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.DevicePushToken", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.LegalConsent", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.MapData", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "Server") + .WithOne() + .HasForeignKey("RustRconServerManager.Backend.Models.MapData", "ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ModeratorPagePermission", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", "User") + .WithMany("PagePermissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ModeratorServerPermission", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "RconServer") + .WithMany("ModeratorPermissions") + .HasForeignKey("RconServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", "User") + .WithMany("ServerPermissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RconServer"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PanelSettings", b => + { + b.HasOne("RustRconServerManager.Backend.Models.SystemProfile", "SystemProfile") + .WithOne("PanelSettings") + .HasForeignKey("RustRconServerManager.Backend.Models.PanelSettings", "SystemProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SystemProfile"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PlayerReport", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", "ReviewedBy") + .WithMany() + .HasForeignKey("ReviewedByUserId"); + + b.Navigation("ReviewedBy"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.PresetCommand", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "RconServer") + .WithMany() + .HasForeignKey("RconServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RconServer"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.RconServer", b => + { + b.HasOne("RustRconServerManager.Backend.Models.SystemProfile", "SystemProfile") + .WithMany() + .HasForeignKey("SystemProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SystemProfile"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ScheduledCommand", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "RconServer") + .WithMany() + .HasForeignKey("RconServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RconServer"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ServerPluginSource", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "RconServer") + .WithMany() + .HasForeignKey("RustServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RconServer"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ServerProtectionSettings", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ServerWebhookSettings", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.Trigger", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "RconServer") + .WithMany() + .HasForeignKey("RconServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RconServer"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.UserNotificationPreference", b => + { + b.HasOne("RustRconServerManager.Backend.Models.RconServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.UserSession", b => + { + b.HasOne("RustRconServerManager.Backend.Models.ApplicationUser", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.ApplicationUser", b => + { + b.Navigation("PagePermissions"); + + b.Navigation("ServerPermissions"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.RconServer", b => + { + b.Navigation("ModeratorPermissions"); + }); + + modelBuilder.Entity("RustRconServerManager.Backend.Models.SystemProfile", b => + { + b.Navigation("PanelSettings") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.cs b/RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.cs new file mode 100644 index 0000000..eb6ab2a --- /dev/null +++ b/RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RustRconServerManager.Backend.Migrations +{ + /// + public partial class AddHasChosenUsernameToApplicationUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HasChosenUsername", + table: "AspNetUsers", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HasChosenUsername", + table: "AspNetUsers"); + } + } +} diff --git a/RustRconServerManager.Backend/Migrations/AppDbContextModelSnapshot.cs b/RustRconServerManager.Backend/Migrations/AppDbContextModelSnapshot.cs index 71ff333..0a02fdb 100644 --- a/RustRconServerManager.Backend/Migrations/AppDbContextModelSnapshot.cs +++ b/RustRconServerManager.Backend/Migrations/AppDbContextModelSnapshot.cs @@ -225,6 +225,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailConfirmed") .HasColumnType("tinyint(1)"); + b.Property("HasChosenUsername") + .HasColumnType("tinyint(1)"); + b.Property("IsModerator") .HasColumnType("tinyint(1)"); diff --git a/RustRconServerManager.Backend/Models/ApplicationUser.cs b/RustRconServerManager.Backend/Models/ApplicationUser.cs index 9d51da9..1b0db74 100644 --- a/RustRconServerManager.Backend/Models/ApplicationUser.cs +++ b/RustRconServerManager.Backend/Models/ApplicationUser.cs @@ -18,6 +18,14 @@ public class ApplicationUser:IdentityUser /// public string? DisplayName { get; set; } public string? Website { get; set; } + + /// + /// True once this account has an explicitly chosen username (set at creation for every + /// account going forward). Accounts created before username-based login existed had + /// their username silently set equal to their email - this stays false for those until + /// they pick a real one via the one-time forced prompt (see MainLayout). + /// + public bool HasChosenUsername { get; set; } = false; public string? DiscordId { get; set; } public string? SteamId { get; set; } public string? SessionHash { get; set; } // Legacy single-session support, kept for backwards compatibility diff --git a/RustRconServerManager.Backend/Program.cs b/RustRconServerManager.Backend/Program.cs index abd48d6..941a295 100644 --- a/RustRconServerManager.Backend/Program.cs +++ b/RustRconServerManager.Backend/Program.cs @@ -121,7 +121,11 @@ { options.Password.RequireDigit = true; options.Password.RequiredLength = 6; - options.User.RequireUniqueEmail = true; + // Email is optional (login is by username) - RequireUniqueEmail would otherwise + // reject account creation entirely whenever no email is provided at all. + // Uniqueness among accounts that DO have one is still enforced manually where + // an email is set/changed (see AuthController.Setup, ModeratorController, SecurityController.ChangeEmail). + options.User.RequireUniqueEmail = false; }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); diff --git a/RustRconServerManager.Backend/SignalRHubs/LiveChatHub.cs b/RustRconServerManager.Backend/SignalRHubs/LiveChatHub.cs index 878aba2..0040853 100644 --- a/RustRconServerManager.Backend/SignalRHubs/LiveChatHub.cs +++ b/RustRconServerManager.Backend/SignalRHubs/LiveChatHub.cs @@ -56,7 +56,7 @@ public async Task SendMessageToCurrentServer(string message) { ServerId = currentServerId, Username = Context.User?.Identity?.Name - ?? Context.User?.FindFirstValue(ClaimTypes.Email) + ?? Context.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? "Unknown", SteamId = null, // Voeg hier je eigen logica toe als je SteamId kunt resolven Message = message.Trim(), diff --git a/RustRconServerManager.Frontend/Components/Account/RequireDisplayNameGate.razor b/RustRconServerManager.Frontend/Components/Account/RequireDisplayNameGate.razor index 37891f7..241402e 100644 --- a/RustRconServerManager.Frontend/Components/Account/RequireDisplayNameGate.razor +++ b/RustRconServerManager.Frontend/Components/Account/RequireDisplayNameGate.razor @@ -38,7 +38,12 @@ { var info = await Http.GetFromJsonAsync("/api/Account/BasicInfo"); var currentDisplayName = info.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; - needsDisplayName = string.IsNullOrWhiteSpace(currentDisplayName); + var hasChosenUsername = info.TryGetProperty("hasChosenUsername", out var hcu) && hcu.GetBoolean(); + + // Defer to RequireUsernameGate if that one still needs to show too - avoids both + // mandatory gates trying to render at once. It'll show right after that one is + // satisfied and forces its own reload. + needsDisplayName = string.IsNullOrWhiteSpace(currentDisplayName) && hasChosenUsername; } catch (Exception ex) { diff --git a/RustRconServerManager.Frontend/Components/Account/RequireUsernameGate.razor b/RustRconServerManager.Frontend/Components/Account/RequireUsernameGate.razor new file mode 100644 index 0000000..c2f3f0e --- /dev/null +++ b/RustRconServerManager.Frontend/Components/Account/RequireUsernameGate.razor @@ -0,0 +1,86 @@ +@* Blocks the panel with a non-dismissible modal until the current user has explicitly + chosen a username (login moved from email to username). Only ever triggers for accounts + created before that existed, whose username was silently set equal to their email. *@ +@using RustRconServerManager.Shared.Account +@inject HttpClient Http +@inject NavigationManager NavigationManager + +@if (needsUsername) +{ + + +

+ Please choose a username. You'll use it to log in from now on instead of your email address. +

+ + + + @if (errorMessage != null) + { +
@errorMessage
+ } +
+ + Save + +
+} + +@code { + private bool needsUsername = false; + private string username = string.Empty; + private bool isSaving = false; + private string? errorMessage; + + protected override async Task OnInitializedAsync() + { + try + { + var info = await Http.GetFromJsonAsync("/api/Account/BasicInfo"); + var hasChosenUsername = info.TryGetProperty("hasChosenUsername", out var hcu) && hcu.GetBoolean(); + needsUsername = !hasChosenUsername; + } + catch (Exception ex) + { + Console.WriteLine($"[RequireUsernameGate] Error checking username: {ex.Message}"); + } + } + + private async Task Save() + { + if (string.IsNullOrWhiteSpace(username)) + { + errorMessage = "Please enter a username."; + return; + } + + try + { + isSaving = true; + errorMessage = null; + + var response = await Http.PutAsJsonAsync("/api/Account/Username", new Account_SetUsernameDTO { Username = username }); + + if (response.IsSuccessStatusCode) + { + needsUsername = false; + // Force a full reload so anything that already fetched BasicInfo before + // the username existed (navbar, the display-name gate) picks it up. + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + else + { + errorMessage = await response.Content.ReadAsStringAsync(); + } + } + catch (Exception ex) + { + errorMessage = $"Error: {ex.Message}"; + } + finally + { + isSaving = false; + } + } +} diff --git a/RustRconServerManager.Frontend/Components/Layout/Navigation/TopNavbar.razor b/RustRconServerManager.Frontend/Components/Layout/Navigation/TopNavbar.razor index 46f06df..2f7c22a 100644 --- a/RustRconServerManager.Frontend/Components/Layout/Navigation/TopNavbar.razor +++ b/RustRconServerManager.Frontend/Components/Layout/Navigation/TopNavbar.razor @@ -136,7 +136,14 @@ {
@(UserDisplayName ?? "Unknown")
-
@(UserEmail ?? "Unknown")
+ @if (!string.IsNullOrEmpty(UserUsername)) + { +
@UserUsername
+ } + @if (!string.IsNullOrEmpty(UserEmail)) + { +
@UserEmail
+ }
@if (!IsModerator) @@ -146,7 +153,7 @@ } - Security + Account Settings @if (!IsModerator) { @@ -178,6 +185,7 @@ private bool IsLoading = true; private bool IsLoadingUserInfo = true; private string? UserEmail = null; + private string? UserUsername = null; private string? UserDisplayName = null; private bool IsModerator = false; private bool ActiveServerIsConnected => Servers?.FirstOrDefault(s => s.ServerId == ActiveServerId)?.IsConnected == true; @@ -310,7 +318,8 @@ var response = await Http.GetFromJsonAsync("/api/Account/BasicInfo"); if (response.ValueKind != System.Text.Json.JsonValueKind.Undefined) { - UserEmail = response.GetProperty("email").GetString(); + UserEmail = response.TryGetProperty("email", out var em) ? em.GetString() : null; + UserUsername = response.TryGetProperty("username", out var un) ? un.GetString() : null; UserDisplayName = response.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; IsModerator = response.GetProperty("isModerator").GetBoolean(); } diff --git a/RustRconServerManager.Frontend/Layout/MainLayout.razor b/RustRconServerManager.Frontend/Layout/MainLayout.razor index 8e99c34..e7449d7 100644 --- a/RustRconServerManager.Frontend/Layout/MainLayout.razor +++ b/RustRconServerManager.Frontend/Layout/MainLayout.razor @@ -7,8 +7,10 @@ @inject NavigationManager NavigationManager
- @* Blocks the panel with a one-time modal for accounts created before display names - became mandatory, until they set one. *@ + @* Blocks the panel with one-time modals for accounts created before username-based + login/mandatory display names existed, until they set one. Username first, since + RequireDisplayNameGate defers to it. *@ + @* Top Navbar *@ diff --git a/RustRconServerManager.Frontend/Pages/Login.razor b/RustRconServerManager.Frontend/Pages/Login.razor index 0721772..6b3f895 100644 --- a/RustRconServerManager.Frontend/Pages/Login.razor +++ b/RustRconServerManager.Frontend/Pages/Login.razor @@ -36,9 +36,9 @@ @if (!requires2FA) {
- + type="text" name="username" placeholder="Username" autocomplete="username" @onkeypress="HandleKeyPress" />
- - Username + + type="text" readonly />
@@ -106,7 +106,7 @@ @code { private bool isLoggingIn = false; private bool requires2FA = false; - private string email = string.Empty; + private string username = string.Empty; private string password = string.Empty; private string twoFactorCode = string.Empty; private string? errorMessage; @@ -154,7 +154,7 @@ var loginDto = new Authorization_UserLoginDTO { - Email = email, + Username = username, Password = password, TwoFactorCode = requires2FA ? twoFactorCode : null }; diff --git a/RustRconServerManager.Frontend/Pages/Security.razor b/RustRconServerManager.Frontend/Pages/Security.razor index 9504a66..1eb66d7 100644 --- a/RustRconServerManager.Frontend/Pages/Security.razor +++ b/RustRconServerManager.Frontend/Pages/Security.razor @@ -6,10 +6,9 @@ @using RustRconServerManager.Frontend.Services @inject HttpClient Http @inject IJSRuntime JS -@inject NavigationHelper NavHelper @attribute [Authorize] -Security Settings +Account Settings
@@ -23,6 +22,17 @@ }
+
+ +
+ + Save +
+ Used to log in instead of your email address. +
+ +
+
@@ -36,11 +46,11 @@
- +
- +
@@ -52,7 +62,7 @@ Change Email - You'll be logged out and need to sign in again with your new email. + Only needed for password-recovery emails - not required to use the panel.
@@ -215,6 +225,7 @@ private bool isChangingPassword = false; private bool isChangingEmail = false; private bool isSavingDisplayName = false; + private bool isSavingUsername = false; private bool isEnabling2FA = false; private bool isDisabling2FA = false; @@ -226,6 +237,7 @@ private bool accountError; private string currentEmail = string.Empty; + private string username = string.Empty; private string displayName = string.Empty; private string qrCodeUri = string.Empty; @@ -243,7 +255,8 @@ try { var info = await Http.GetFromJsonAsync("/api/Account/BasicInfo"); - currentEmail = info.GetProperty("email").GetString() ?? string.Empty; + currentEmail = info.TryGetProperty("email", out var em) ? em.GetString() ?? string.Empty : string.Empty; + username = info.TryGetProperty("username", out var un) ? un.GetString() ?? string.Empty : string.Empty; displayName = info.TryGetProperty("displayName", out var dn) ? dn.GetString() ?? string.Empty : string.Empty; } catch (Exception ex) @@ -283,6 +296,37 @@ } } + private async Task SaveUsername() + { + try + { + isSavingUsername = true; + accountMessage = null; + + var response = await Http.PutAsJsonAsync("/api/Account/Username", new Account_SetUsernameDTO { Username = username }); + + if (response.IsSuccessStatusCode) + { + accountMessage = "Username updated successfully!"; + accountError = false; + } + else + { + accountMessage = await response.Content.ReadAsStringAsync(); + accountError = true; + } + } + catch (Exception ex) + { + accountMessage = $"Error: {ex.Message}"; + accountError = true; + } + finally + { + isSavingUsername = false; + } + } + private async Task ChangeEmail() { try @@ -294,15 +338,10 @@ if (response.IsSuccessStatusCode) { - accountMessage = "Email changed successfully! Logging you out..."; + accountMessage = "Email changed successfully!"; accountError = false; changeEmailDto = new(); - - // The backend already revoked this session and cleared the auth cookie - // as part of the email change itself - calling /api/Auth/logout here - // would just fail, since the (now stale) credential it needs is already gone. - await Task.Delay(2000); - NavHelper.NavigateTo("login", true); + await LoadAccountInfo(); } else { diff --git a/RustRconServerManager.Frontend/Pages/Setup.razor b/RustRconServerManager.Frontend/Pages/Setup.razor index a31dfc8..4c1ef69 100644 --- a/RustRconServerManager.Frontend/Pages/Setup.razor +++ b/RustRconServerManager.Frontend/Pages/Setup.razor @@ -52,6 +52,13 @@ else if (currentStep == SetupStep.AccountSetup) }
+
+ + +
+
- + + Only needed for password-recovery emails. You can add or change it later.
@@ -122,6 +130,7 @@ else if (currentStep == SetupStep.AccountSetup) private SetupStep currentStep = SetupStep.Welcome; private bool isProcessing = false; + private string username = string.Empty; private string email = string.Empty; private string displayName = string.Empty; private string password = string.Empty; @@ -175,9 +184,9 @@ else if (currentStep == SetupStep.AccountSetup) successMessage = null; // Basic validation - if (string.IsNullOrWhiteSpace(email)) + if (string.IsNullOrWhiteSpace(username)) { - errorMessage = "Please enter an email address"; + errorMessage = "Please enter a username"; isProcessing = false; return; } @@ -205,7 +214,8 @@ else if (currentStep == SetupStep.AccountSetup) var setupRequest = new Authorization_SetupRequestDTO { - Email = email, + Username = username, + Email = string.IsNullOrWhiteSpace(email) ? null : email, DisplayName = displayName, Password = password, ConfirmPassword = confirmPassword, diff --git a/RustRconServerManager.Frontend/wwwroot/index.html b/RustRconServerManager.Frontend/wwwroot/index.html index d22531c..f20ed4a 100644 --- a/RustRconServerManager.Frontend/wwwroot/index.html +++ b/RustRconServerManager.Frontend/wwwroot/index.html @@ -13,8 +13,31 @@ - - + + + +