From 5b1e97c0cbe083532f9d747c6a0115981a1571be Mon Sep 17 00:00:00 2001
From: Xenne <144433308+Xenne93@users.noreply.github.com>
Date: Wed, 19 Aug 2026 15:55:50 +0200
Subject: [PATCH] Fix Discord emoji encoding, add instant purge, mandatory
display names, email change
- Fix mojibake in Discord embed messages: the offline/online status emoji
had been mangled into a UTF-8-as-Windows-1252 double-encoding at some
point in the source file, showing as garbled characters in Discord
- Panel Settings: add a "Delete All Now" button next to the existing
older-than-X-days purge control, for immediately clearing every record
in a category on demand
- Add a mandatory Display Name, shown across the panel (navbar, moderator
lists, audit log) instead of the user's email address:
- Renamed the existing (optional) ApplicationUser.Nickname field to
DisplayName, with a migration and all call sites updated
- Required at initial admin setup
- Editable from Security Settings
- Existing accounts created before this field existed are prompted with
a blocking modal on next login until they set one
- Add email address changes from Security Settings, gated behind the
current password. Also revokes the current session and clears the auth
cookie server-side as part of the change - SecurityBindingMiddleware
re-resolves the user by their email claim on every request, so leaving
the old session's cookie in place would 401 all further requests
(including the follow-up logout call and even loading the login page)
until the browser's cookies were cleared manually
- TwModal: add an optional ZIndexClass parameter so the new mandatory
display-name gate can render above other in-app modals (e.g. the
first-run "no servers yet" welcome dialog) instead of stacking behind
them
---
.../Controllers/AccountController.cs | 31 +-
.../Controllers/AuthController.cs | 6 +
.../Controllers/ModeratorController.cs | 8 +-
.../Controllers/PanelSettingsController.cs | 11 +-
.../Controllers/SecurityController.cs | 71 +
...43_RenameNicknameToDisplayName.Designer.cs | 2284 +++++++++++++++++
...60819133143_RenameNicknameToDisplayName.cs | 28 +
.../Migrations/AppDbContextModelSnapshot.cs | 6 +-
.../Models/ApplicationUser.cs | 8 +-
.../Services/DiscordWebhookService.cs | 24 +-
.../Account/RequireDisplayNameGate.razor | 85 +
.../Layout/Navigation/TopNavbar.razor | 9 +-
.../Components/UI/TwModal.razor | 9 +-
.../Layout/MainLayout.razor | 5 +
.../Pages/PanelSettings.razor | 50 +
.../Pages/Security.razor | 139 +
.../Pages/Setup.razor | 16 +
.../wwwroot/css/tailwind-output.css | 2 +-
.../Account/Account_AccountInformationDTO.cs | 2 +-
.../Account/Account_SetDisplayNameDTO.cs | 6 +
.../Authorization_SetupRequestDTO.cs | 1 +
.../Scheduler/PanelSettingsDto.cs | 5 +
.../Security/Security_ChangeEmailDTO.cs | 7 +
23 files changed, 2781 insertions(+), 32 deletions(-)
create mode 100644 RustRconServerManager.Backend/Migrations/20260819133143_RenameNicknameToDisplayName.Designer.cs
create mode 100644 RustRconServerManager.Backend/Migrations/20260819133143_RenameNicknameToDisplayName.cs
create mode 100644 RustRconServerManager.Frontend/Components/Account/RequireDisplayNameGate.razor
create mode 100644 RustRconServerManager.Shared/Account/Account_SetDisplayNameDTO.cs
create mode 100644 RustRconServerManager.Shared/Security/Security_ChangeEmailDTO.cs
diff --git a/RustRconServerManager.Backend/Controllers/AccountController.cs b/RustRconServerManager.Backend/Controllers/AccountController.cs
index 20acf48..d67a0c8 100644
--- a/RustRconServerManager.Backend/Controllers/AccountController.cs
+++ b/RustRconServerManager.Backend/Controllers/AccountController.cs
@@ -42,7 +42,7 @@ public IActionResult GetBasicInfo()
return Ok(new
{
Email = user.Email,
- Nickname = user.Nickname,
+ DisplayName = user.DisplayName,
IsModerator = user.IsModerator,
isAdmin = user.isAdmin
});
@@ -94,7 +94,7 @@ public IActionResult AccountInformation()
Account_AccountInformationDTO dto = new Account_AccountInformationDTO();
dto.Email = accountInformation.Email;
- dto.Nickname = accountInformation.Nickname;
+ dto.DisplayName = accountInformation.DisplayName;
dto.CreatedAt = accountInformation.CreatedAt;
dto.isAdmin = accountInformation.isAdmin;
dto.IsModerator = accountInformation.IsModerator;
@@ -104,8 +104,35 @@ public IActionResult AccountInformation()
return Ok(dto);
}
+ ///
+ /// Sets the current user's display name - shown across the panel (navbar, moderator
+ /// lists, audit log) instead of their email address. Required for every account.
+ ///
+ [HttpPut("DisplayName")]
+ public async Task SetDisplayName([FromBody] Account_SetDisplayNameDTO dto)
+ {
+ var trimmed = dto.DisplayName?.Trim() ?? string.Empty;
+
+ if (string.IsNullOrEmpty(trimmed))
+ return BadRequest("Display name is required.");
+
+ 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;
+ if (string.IsNullOrEmpty(userEmail))
+ return Unauthorized("No email claim found");
+ var user = await _dbContext.Users.FirstOrDefaultAsync(x => x.Email == userEmail);
+ if (user == null)
+ return NotFound();
+
+ user.DisplayName = trimmed;
+ await _dbContext.SaveChangesAsync();
+
+ return Ok(new { DisplayName = user.DisplayName });
+ }
}
}
\ No newline at end of file
diff --git a/RustRconServerManager.Backend/Controllers/AuthController.cs b/RustRconServerManager.Backend/Controllers/AuthController.cs
index d4b639d..332302f 100644
--- a/RustRconServerManager.Backend/Controllers/AuthController.cs
+++ b/RustRconServerManager.Backend/Controllers/AuthController.cs
@@ -73,6 +73,11 @@ public async Task Setup(Authorization_SetupRequestDTO model)
return BadRequest(new { message = "Passwords do not match" });
}
+ if (string.IsNullOrWhiteSpace(model.DisplayName))
+ {
+ return BadRequest(new { message = "Display name is required" });
+ }
+
// Get or create default SystemProfile
var systemProfile = await _dbContext.SystemProfiles.FirstOrDefaultAsync();
if (systemProfile == null)
@@ -108,6 +113,7 @@ public async Task Setup(Authorization_SetupRequestDTO model)
{
UserName = model.Email,
Email = model.Email,
+ DisplayName = model.DisplayName.Trim(),
SystemProfileId = systemProfile.Id,
isAdmin = true,
EmailConfirmed = true, // Auto-confirm email for initial admin
diff --git a/RustRconServerManager.Backend/Controllers/ModeratorController.cs b/RustRconServerManager.Backend/Controllers/ModeratorController.cs
index cc88559..64a985f 100644
--- a/RustRconServerManager.Backend/Controllers/ModeratorController.cs
+++ b/RustRconServerManager.Backend/Controllers/ModeratorController.cs
@@ -47,7 +47,7 @@ public async Task List()
{
Id = m.Id,
Username = m.UserName ?? string.Empty,
- DisplayName = m.Nickname,
+ DisplayName = m.DisplayName,
Email = m.Email,
IsActive = !m.isLoginBlocked,
CreatedAt = m.CreatedAt,
@@ -117,7 +117,7 @@ public async Task Create([FromBody] Moderator_CreateDTO dto)
{
UserName = dto.Username,
Email = dto.Email ?? $"{dto.Username}@moderator.local",
- Nickname = dto.DisplayName,
+ DisplayName = dto.DisplayName,
SystemProfileId = profile.Id,
IsModerator = true,
isLoginBlocked = false,
@@ -150,7 +150,7 @@ public async Task Create([FromBody] Moderator_CreateDTO dto)
{
Id = moderator.Id,
Username = moderator.UserName ?? string.Empty,
- DisplayName = moderator.Nickname,
+ DisplayName = moderator.DisplayName,
Email = moderator.Email,
IsActive = !moderator.isLoginBlocked,
LastLoginAt = moderator.LastLoginAt,
@@ -204,7 +204,7 @@ public async Task Update(string id, [FromBody] Moderator_UpdateDT
}
// Update moderator properties
- moderator.Nickname = dto.DisplayName;
+ moderator.DisplayName = dto.DisplayName;
moderator.Email = dto.Email;
moderator.isLoginBlocked = !dto.IsActive;
diff --git a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs
index 5688bb0..9d2d2fd 100644
--- a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs
+++ b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs
@@ -155,7 +155,7 @@ public async Task PurgeData([FromBody] PurgeDataRequestDto reques
{
try
{
- if (request.OlderThanDays < 1)
+ if (!request.Instant && request.OlderThanDays < 1)
return BadRequest(new { error = "OlderThanDays must be at least 1" });
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
@@ -172,7 +172,9 @@ public async Task PurgeData([FromBody] PurgeDataRequestDto reques
.Select(s => s.Id)
.ToListAsync();
- var cutoff = DateTime.UtcNow.AddDays(-request.OlderThanDays);
+ // When Instant is set, cutoff is "now" so every existing record older-than check
+ // still holds true for anything already in the database.
+ var cutoff = request.Instant ? DateTime.UtcNow : DateTime.UtcNow.AddDays(-request.OlderThanDays);
int deleted = 0;
switch (request.Category)
@@ -217,8 +219,9 @@ public async Task PurgeData([FromBody] PurgeDataRequestDto reques
return BadRequest(new { error = $"Unknown or non-purgeable category: {request.Category}" });
}
- _logger.LogInformation("[PanelSettingsController] User {UserId} purged {Count} records from {Category} (older than {Days} days)",
- userId, deleted, request.Category?.Replace("\r", "").Replace("\n", ""), request.OlderThanDays);
+ _logger.LogInformation("[PanelSettingsController] User {UserId} purged {Count} records from {Category} ({Scope})",
+ userId, deleted, request.Category?.Replace("\r", "").Replace("\n", ""),
+ request.Instant ? "instant - all records" : $"older than {request.OlderThanDays} days");
return Ok(new { deleted, category = request.Category });
}
diff --git a/RustRconServerManager.Backend/Controllers/SecurityController.cs b/RustRconServerManager.Backend/Controllers/SecurityController.cs
index 026c69e..b877dc0 100644
--- a/RustRconServerManager.Backend/Controllers/SecurityController.cs
+++ b/RustRconServerManager.Backend/Controllers/SecurityController.cs
@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
using RustRconServerManager.Backend.Database;
using RustRconServerManager.Backend.Extensions;
using RustRconServerManager.Backend.Models;
using RustRconServerManager.Backend.Helpers;
using RustRconServerManager.Shared.Security;
+using System.Net.Mail;
using System.Text;
using System.Text.Encodings.Web;
@@ -78,6 +80,75 @@ public async Task ChangePassword([FromBody] Security_ChangePasswo
}
}
+ [HttpPost("change-email")]
+ public async Task ChangeEmail([FromBody] Security_ChangeEmailDTO dto)
+ {
+ try
+ {
+ var user = await User.GetUser(_dbContext);
+
+ if (string.IsNullOrWhiteSpace(dto.NewEmail) || !MailAddress.TryCreate(dto.NewEmail, out _))
+ {
+ return BadRequest("A valid email address is required.");
+ }
+
+ var passwordValid = await _userManager.CheckPasswordAsync(user, dto.CurrentPassword);
+ if (!passwordValid)
+ {
+ return BadRequest("Current password is incorrect.");
+ }
+
+ var existingUser = await _userManager.FindByEmailAsync(dto.NewEmail);
+ if (existingUser != null && existingUser.Id != user.Id)
+ {
+ return BadRequest("This email address is already in use.");
+ }
+
+ var setEmailResult = await _userManager.SetEmailAsync(user, dto.NewEmail);
+ if (!setEmailResult.Succeeded)
+ {
+ return BadRequest(string.Join(", ", setEmailResult.Errors.Select(e => e.Description)));
+ }
+
+ await _userManager.SetUserNameAsync(user, dto.NewEmail);
+ 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." });
+ }
+ catch (Exception ex)
+ {
+ return BadRequest(ApiErrorHelper.FormatError("Failed to change email", ex));
+ }
+ }
+
[HttpGet("2fa/status")]
public async Task Get2FAStatus()
{
diff --git a/RustRconServerManager.Backend/Migrations/20260819133143_RenameNicknameToDisplayName.Designer.cs b/RustRconServerManager.Backend/Migrations/20260819133143_RenameNicknameToDisplayName.Designer.cs
new file mode 100644
index 0000000..501b248
--- /dev/null
+++ b/RustRconServerManager.Backend/Migrations/20260819133143_RenameNicknameToDisplayName.Designer.cs
@@ -0,0 +1,2284 @@
+//
+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("20260819133143_RenameNicknameToDisplayName")]
+ partial class RenameNicknameToDisplayName
+ {
+ ///
+ 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("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