From 4c7f5ae93553416f3cd67998b4c7d8e7517e16b0 Mon Sep 17 00:00:00 2001
From: Xenne <144433308+Xenne93@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:24:09 +0200
Subject: [PATCH] Switch login from email to username, rename navbar item, fix
stylesheet reload bug
- Login is now by username instead of email. Email becomes fully optional
everywhere (setup, moderator creation, RequireUniqueEmail disabled) -
only needed if you want the "forgot password" email-recovery flow
- Identity resolution (ClaimsPrincipalExtensions.GetUser,
SecurityBindingMiddleware) now keys on the user's Id instead of email,
since email may not exist. This also means changing your email no
longer invalidates your current session - removed the forced logout
that used to be needed for that
- Fixed a bug this surfaced: SecurityController.ChangeEmail was calling
SetUserNameAsync with the new email, silently overwriting the user's
chosen username on every email change
- Existing accounts (created before username-based login existed, whose
username was silently set equal to their email) are prompted once with
a blocking modal to pick a real username on next login, same pattern as
the existing Display Name gate - added HasChosenUsername to track this
- Added a Username field to Account Settings (Security page) alongside
the existing Display Name/Change Email fields
- Emergency CLI password-reset tool (--reset-password, for admins with no
working SMTP) now looks up the account by username instead of email
- Renamed the "Security" navbar/dropdown item and page title to "Account
Settings", since it now covers general account info too
- Stylesheet loading fix: added onerror-triggered retry-with-backoff (and
eventual full reload) to the two local stylesheet tags, to
recover automatically from a failed CSS request instead of requiring a
manual refresh - most often hit after the browser/machine has been idle
for hours and the network hasn't fully reconnected when the tab wakes up
Tested end-to-end in Docker: legacy account forced through the username
gate and logging in with the old email-shaped username no longer working
afterward, email changes no longer forcing a logout (and username
surviving the change), a fresh setup wizard with no email at all, and
moderator creation without an email.
---
.../Cli/ResetPasswordCli.cs | 14 +-
.../Controllers/AccountController.cs | 75 +-
.../Controllers/AuthController.cs | 37 +-
.../Controllers/DashboardController.cs | 10 +-
.../Controllers/ModeratorController.cs | 4 +-
.../Controllers/SecurityController.cs | 35 +-
.../Extensions/ClaimsPrincipalExtensions.cs | 6 +-
.../Middleware/SecurityBindingMiddleware.cs | 6 +-
...hosenUsernameToApplicationUser.Designer.cs | 2287 +++++++++++++++++
...6_AddHasChosenUsernameToApplicationUser.cs | 29 +
.../Migrations/AppDbContextModelSnapshot.cs | 3 +
.../Models/ApplicationUser.cs | 8 +
RustRconServerManager.Backend/Program.cs | 6 +-
.../SignalRHubs/LiveChatHub.cs | 2 +-
.../Account/RequireDisplayNameGate.razor | 7 +-
.../Account/RequireUsernameGate.razor | 86 +
.../Layout/Navigation/TopNavbar.razor | 15 +-
.../Layout/MainLayout.razor | 6 +-
.../Pages/Login.razor | 14 +-
.../Pages/Security.razor | 65 +-
.../Pages/Setup.razor | 18 +-
.../wwwroot/index.html | 27 +-
.../Account/Account_AccountInformationDTO.cs | 3 +-
.../Account/Account_SetUsernameDTO.cs | 6 +
.../Authorization_SetupRequestDTO.cs | 3 +-
.../Authorization_UserLoginDTO.cs | 2 +-
26 files changed, 2661 insertions(+), 113 deletions(-)
create mode 100644 RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.Designer.cs
create mode 100644 RustRconServerManager.Backend/Migrations/20260819151536_AddHasChosenUsernameToApplicationUser.cs
create mode 100644 RustRconServerManager.Frontend/Components/Account/RequireUsernameGate.razor
create mode 100644 RustRconServerManager.Shared/Account/Account_SetUsernameDTO.cs
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