diff --git a/CannedNet.Admin/Components/Pages/Login.razor b/CannedNet.Admin/Components/Pages/Login.razor
index d0530c5..2892aea 100644
--- a/CannedNet.Admin/Components/Pages/Login.razor
+++ b/CannedNet.Admin/Components/Pages/Login.razor
@@ -1,52 +1,33 @@
@page "/login"
@rendermode InteractiveServer
-@using CannedNet.Admin.Services
-@inject AdminAuthService AuthService
@inject NavigationManager Navigation
Login
-@if (AuthService.HasAnyUser() == false)
-{
-
-
-
-
-
No Admin Accounts
-
No admin accounts exist yet. Complete the first-time setup.
-
Go to Setup
-
+
@code {
private string? errorMessage;
diff --git a/CannedNet.Admin/Components/Pages/Rooms/Create.razor b/CannedNet.Admin/Components/Pages/Rooms/Create.razor
deleted file mode 100644
index b41096d..0000000
--- a/CannedNet.Admin/Components/Pages/Rooms/Create.razor
+++ /dev/null
@@ -1,176 +0,0 @@
-@page "/rooms/create"
-@rendermode InteractiveServer
-
-
Create Room
-
-
Create Room
-
Create a new room and optionally a default subroom.
-
-
-
-@code {
- [Inject] private HttpClient Http { get; set; } = default!;
-
- private bool creating;
- private string? message;
- private Room? createdRoom;
-
- private int roomId;
- private string name = "";
- private string description = "";
- private int creatorAccountId;
- private string imageName = "";
- private int accessibility;
- private bool createSubRoom = true;
- private string subRoomName = "";
- private int maxPlayers = 16;
- private bool supportsVRLow = true;
- private bool supportsQuest2 = true;
- private bool supportsMobile = true;
- private bool supportsScreens = true;
- private bool supportsWalkVR = true;
- private bool supportsTeleportVR = true;
- private bool supportsJuniors = true;
-
- private async Task CreateRoom()
- {
- creating = true;
- message = null;
- try
- {
- var payload = new
- {
- roomId = roomId > 0 ? roomId : 0,
- name,
- description,
- creatorAccountId,
- imageName,
- accessibility,
- createSubRoom,
- subRoomName = createSubRoom ? (string.IsNullOrWhiteSpace(subRoomName) ? name : subRoomName) : "",
- maxPlayers
- };
- var response = await Http.PostAsJsonAsync("/admin/api/rooms", payload);
- response.EnsureSuccessStatusCode();
- createdRoom = await response.Content.ReadFromJsonAsync
();
- message = $"Room '{createdRoom?.Name}' created successfully!";
- }
- catch (Exception ex)
- {
- message = $"Error: {ex.Message}";
- }
- finally
- {
- creating = false;
- }
- }
-}
diff --git a/CannedNet.Admin/Components/Pages/Setup/AdminAccounts.razor b/CannedNet.Admin/Components/Pages/Setup/AdminAccounts.razor
deleted file mode 100644
index c914b88..0000000
--- a/CannedNet.Admin/Components/Pages/Setup/AdminAccounts.razor
+++ /dev/null
@@ -1,144 +0,0 @@
-@page "/setup/accounts"
-@attribute [Authorize(Policy = "OwnerOnly")]
-@rendermode InteractiveServer
-@using CannedNet.Admin.Models
-@using CannedNet.Admin.Services
-@inject AdminAuthService AuthService
-
-Admin Accounts
-
-Admin Accounts
-Manage admin panel accounts. Only the owner can create or delete accounts.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | ID |
- Username |
- Role |
- Created |
- Actions |
-
-
-
- @foreach (var user in AuthService.GetAll())
- {
-
- | @user.Id |
- @user.Username |
-
- @if (user.IsOwner)
- {
- | Owner |
- }
- else
- {
- Admin |
- }
-
- @user.CreatedAt.ToString("g") |
-
- @if (!user.IsOwner)
- {
-
- }
- |
-
- }
-
-
-
-
-
-@code {
- private string newUsername = "";
- private string newPassword = "";
- private string newConfirmPassword = "";
- private bool creating;
- private string? createMessage;
-
- private async Task CreateAccount()
- {
- creating = true;
- createMessage = null;
-
- try
- {
- if (string.IsNullOrWhiteSpace(newUsername))
- {
- createMessage = "Username is required.";
- return;
- }
- if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
- {
- createMessage = "Password must be at least 6 characters.";
- return;
- }
- if (newPassword != newConfirmPassword)
- {
- createMessage = "Passwords do not match.";
- return;
- }
- if (AuthService.GetByUsername(newUsername) != null)
- {
- createMessage = "Username already exists.";
- return;
- }
-
- AuthService.CreateUser(newUsername, newPassword, isOwner: false);
- createMessage = "Admin account created successfully!";
- newUsername = "";
- newPassword = "";
- newConfirmPassword = "";
- }
- catch (Exception ex)
- {
- createMessage = $"Error: {ex.Message}";
- }
- finally
- {
- creating = false;
- }
- }
-
- private async Task DeleteUser(int id)
- {
- var confirmed = await JSRuntime.InvokeAsync("confirm", "Delete this admin account?");
- if (!confirmed) return;
- AuthService.DeleteUser(id);
- }
-
- [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
-}
diff --git a/CannedNet.Admin/Components/Pages/Setup/Index.razor b/CannedNet.Admin/Components/Pages/Setup/Index.razor
deleted file mode 100644
index 1be3917..0000000
--- a/CannedNet.Admin/Components/Pages/Setup/Index.razor
+++ /dev/null
@@ -1,110 +0,0 @@
-@page "/setup"
-@rendermode InteractiveServer
-@using CannedNet.Admin.Services
-@inject AdminAuthService AuthService
-@inject NavigationManager Navigation
-
-First-Time Setup
-
-
-
-
-
-
- @if (completed)
- {
-
-
Owner account created!
-
You can now log in with your new admin account.
-
- }
- else if (AuthService.HasAnyUser())
- {
-
- }
- else
- {
-
Create the initial owner account. This account will have full access and can create additional admin accounts.
-
- }
-
-
-
-
-
-@code {
- private string username = "";
- private string password = "";
- private string confirmPassword = "";
- private bool creating;
- private bool completed;
- private string? message;
-
- private async Task CreateOwner()
- {
- creating = true;
- message = null;
-
- try
- {
- if (string.IsNullOrWhiteSpace(username))
- {
- message = "Username is required.";
- return;
- }
- if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
- {
- message = "Password must be at least 6 characters.";
- return;
- }
- if (password != confirmPassword)
- {
- message = "Passwords do not match.";
- return;
- }
- if (AuthService.HasAnyUser())
- {
- message = "Admin accounts already exist.";
- return;
- }
- if (AuthService.GetByUsername(username) != null)
- {
- message = "Username already taken.";
- return;
- }
-
- AuthService.CreateUser(username, password, isOwner: true);
- completed = true;
- }
- catch (Exception ex)
- {
- message = $"Error: {ex.Message}";
- }
- finally
- {
- creating = false;
- }
- }
-}
diff --git a/CannedNet.Admin/Components/Pages/Setup/Nameserver.razor b/CannedNet.Admin/Components/Pages/Setup/Nameserver.razor
deleted file mode 100644
index 7de3018..0000000
--- a/CannedNet.Admin/Components/Pages/Setup/Nameserver.razor
+++ /dev/null
@@ -1,110 +0,0 @@
-@page "/setup/nameserver"
-@rendermode InteractiveServer
-@using CannedNet.Admin.Models
-@using System.Text.Json
-
-Nameserver Setup
-
-Nameserver Endpoints
-Configure the base server URL the game client connects to. All endpoints are derived from this URL.
-
-@if (loading)
-{
-
-}
-else if (!string.IsNullOrEmpty(error))
-{
- @error
-}
-else
-{
-
-}
-
-@code {
- [Inject] private HttpClient Http { get; set; } = default!;
-
- private bool loading = true;
- private bool saving;
- private bool saveError;
- private string? error;
- private string? saveMessage;
- private string baseUrl = "";
-
- protected override async Task OnInitializedAsync()
- {
- try
- {
- var json = await Http.GetStringAsync("/admin/api/endpoints");
- baseUrl = EndpointDefinitions.ExtractBaseUrl(json);
- }
- catch (Exception ex)
- {
- error = $"Failed to load: {ex.Message}";
- }
- finally
- {
- loading = false;
- }
- }
-
- private async Task Save()
- {
- saving = true;
- saveMessage = null;
- saveError = false;
- try
- {
- var json = EndpointDefinitions.BuildEndpointsJson(baseUrl);
- var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
- var response = await Http.PutAsync("/admin/api/endpoints", content);
- response.EnsureSuccessStatusCode();
- saveMessage = "Saved successfully!";
- }
- catch (Exception ex)
- {
- saveMessage = $"Error: {ex.Message}";
- saveError = true;
- }
- finally
- {
- saving = false;
- }
- }
-}
diff --git a/CannedNet.Admin/Models/AdminUser.cs b/CannedNet.Admin/Models/AdminUser.cs
deleted file mode 100644
index 246db69..0000000
--- a/CannedNet.Admin/Models/AdminUser.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace CannedNet.Admin.Models;
-
-public class AdminUser
-{
- [JsonPropertyName("id")]
- public int Id { get; set; }
-
- [JsonPropertyName("username")]
- public string Username { get; set; } = "";
-
- [JsonPropertyName("passwordHash")]
- public string PasswordHash { get; set; } = "";
-
- [JsonPropertyName("isOwner")]
- public bool IsOwner { get; set; }
-
- [JsonPropertyName("createdAt")]
- public DateTime CreatedAt { get; set; }
-}
diff --git a/CannedNet.Admin/Program.cs b/CannedNet.Admin/Program.cs
index 4182f83..f5cfcfb 100644
--- a/CannedNet.Admin/Program.cs
+++ b/CannedNet.Admin/Program.cs
@@ -1,8 +1,9 @@
using System.Security.Claims;
+using System.Text;
+using System.Text.Json;
using CannedNet.Admin.Components;
using CannedNet.Admin.Services;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
@@ -23,22 +24,17 @@ builder.Services.AddAuthentication("AdminCookie")
options.SlidingExpiration = true;
});
-builder.Services.AddAuthorization(options =>
-{
- options.AddPolicy("OwnerOnly", policy => policy.RequireClaim("role", "Owner"));
-});
+builder.Services.AddAuthorization();
-builder.Services.AddSingleton();
var imagesBaseUrl = builder.Configuration.GetValue("ImagesBaseUrl") ?? "http://localhost:5000/img";
builder.Services.AddSingleton(new ImageUrlService(imagesBaseUrl));
builder.Services.AddHttpContextAccessor();
-
var serverUrl = builder.Configuration.GetValue("ServerBaseUrl") ?? "http://localhost:5000";
-builder.Services.AddTransient();
+builder.Services.AddTransient();
builder.Services.AddScoped(sp =>
{
- var handler = sp.GetRequiredService();
+ var handler = sp.GetRequiredService();
handler.InnerHandler = new SocketsHttpHandler();
return new HttpClient(handler) { BaseAddress = new Uri(serverUrl) };
});
@@ -47,7 +43,7 @@ var app = builder.Build();
// --- Admin Auth Endpoints ---
-app.MapPost("/admin/login", async (HttpContext context, AdminAuthService authService) =>
+app.MapPost("/admin/login", async (HttpContext context) =>
{
var form = await context.Request.ReadFormAsync();
var username = form["username"].FirstOrDefault() ?? "";
@@ -59,20 +55,55 @@ app.MapPost("/admin/login", async (HttpContext context, AdminAuthService authSer
return;
}
- var user = authService.GetByUsername(username);
- if (user == null || !authService.VerifyPassword(password, user.PasswordHash))
+ var serverUrl = app.Services.GetRequiredService().GetValue("ServerBaseUrl") ?? "http://localhost:5000";
+ using var httpClient = new HttpClient { BaseAddress = new Uri(serverUrl) };
+ var tokenResponse = await httpClient.PostAsync("/auth/connect/token", new FormUrlEncodedContent(new Dictionary
+ {
+ ["grant_type"] = "password",
+ ["username"] = username,
+ ["password"] = password
+ }));
+
+ if (!tokenResponse.IsSuccessStatusCode)
{
context.Response.Redirect("/login?error=invalid");
return;
}
- var claims = new[]
+ var json = await tokenResponse.Content.ReadAsStringAsync();
+ using var doc = System.Text.Json.JsonDocument.Parse(json);
+ var accessToken = doc.RootElement.GetProperty("access_token").GetString();
+
+ if (string.IsNullOrEmpty(accessToken))
{
- new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
- new Claim(ClaimTypes.Name, user.Username),
- new Claim("role", user.IsOwner ? "Owner" : "Admin")
+ context.Response.Redirect("/login?error=invalid");
+ return;
+ }
+
+ var payloadJson = DecodeJwtPayload(accessToken);
+ using var payload = JsonDocument.Parse(payloadJson);
+ var accountId = payload.RootElement.TryGetProperty("sub", out var sub) ? sub.GetString() ?? "" : "";
+ var role = "";
+ if (payload.RootElement.TryGetProperty("role", out var r))
+ {
+ if (r.ValueKind == JsonValueKind.String)
+ role = r.GetString() ?? "";
+ else if (r.ValueKind == JsonValueKind.Array)
+ role = string.Join(",", r.EnumerateArray().Select(x => x.GetString() ?? ""));
+ }
+ var name = payload.RootElement.TryGetProperty("platform_id", out var pid) ? pid.GetString() ?? username : username;
+
+ var claims = new List
+ {
+ new(ClaimTypes.NameIdentifier, accountId),
+ new(ClaimTypes.Name, name),
+ new("access_token", accessToken)
};
+ // Map JWT roles for admin app authorization
+ if (role.Split(',', StringSplitOptions.RemoveEmptyEntries).Any(rr => rr == "developer" || rr == "admin"))
+ claims.Add(new Claim(ClaimTypes.Role, "Admin"));
+
var identity = new ClaimsIdentity(claims, "AdminCookie");
var principal = new ClaimsPrincipal(identity);
@@ -111,3 +142,17 @@ app.MapRazorComponents()
.AddInteractiveServerRenderMode();
app.Run();
+
+static string DecodeJwtPayload(string jwt)
+{
+ var parts = jwt.Split('.');
+ if (parts.Length < 2) return "{}";
+ var payload = parts[1];
+ switch (payload.Length % 4)
+ {
+ case 2: payload += "=="; break;
+ case 3: payload += "="; break;
+ }
+ var bytes = Convert.FromBase64String(payload);
+ return Encoding.UTF8.GetString(bytes);
+}
diff --git a/CannedNet.Admin/Services/AdminApiKeyHandler.cs b/CannedNet.Admin/Services/AdminApiKeyHandler.cs
deleted file mode 100644
index a6fe317..0000000
--- a/CannedNet.Admin/Services/AdminApiKeyHandler.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace CannedNet.Admin.Services;
-
-public class AdminApiKeyHandler : DelegatingHandler
-{
- private readonly string _apiKey;
-
- public AdminApiKeyHandler(IConfiguration config)
- {
- _apiKey = config.GetValue("Admin:ApiKey")
- ?? config.GetValue("AdminApiKey")
- ?? "";
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- if (!string.IsNullOrEmpty(_apiKey))
- request.Headers.Add("X-Admin-Api-Key", _apiKey);
-
- return await base.SendAsync(request, cancellationToken);
- }
-}
diff --git a/CannedNet.Admin/Services/AdminAuthService.cs b/CannedNet.Admin/Services/AdminAuthService.cs
deleted file mode 100644
index 943fbc5..0000000
--- a/CannedNet.Admin/Services/AdminAuthService.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-using CannedNet.Admin.Models;
-
-namespace CannedNet.Admin.Services;
-
-public class AdminAuthService
-{
- private readonly string _filePath;
- private List _users = new();
- private readonly object _lock = new();
- private int _nextId = 1;
-
- public AdminAuthService(IWebHostEnvironment env)
- {
- _filePath = Path.Combine(env.ContentRootPath, "admin_users.json");
- Load();
- }
-
- public bool HasAnyUser()
- {
- lock (_lock) { return _users.Count > 0; }
- }
-
- public AdminUser? GetById(int id)
- {
- lock (_lock) { return _users.FirstOrDefault(u => u.Id == id); }
- }
-
- public AdminUser? GetByUsername(string username)
- {
- lock (_lock) { return _users.FirstOrDefault(u => u.Username.Equals(username, StringComparison.OrdinalIgnoreCase)); }
- }
-
- public List GetAll()
- {
- lock (_lock) { return _users.OrderBy(u => u.Id).ToList(); }
- }
-
- public bool IsOwner(int id)
- {
- lock (_lock) { return _users.Any(u => u.Id == id && u.IsOwner); }
- }
-
- public AdminUser CreateUser(string username, string password, bool isOwner)
- {
- lock (_lock)
- {
- var user = new AdminUser
- {
- Id = _nextId++,
- Username = username,
- PasswordHash = HashPassword(password),
- IsOwner = isOwner,
- CreatedAt = DateTime.UtcNow
- };
- _users.Add(user);
- Save();
- return user;
- }
- }
-
- public bool ChangePassword(int id, string oldPassword, string newPassword)
- {
- lock (_lock)
- {
- var user = _users.FirstOrDefault(u => u.Id == id);
- if (user == null) return false;
- if (user.PasswordHash != HashPassword(oldPassword)) return false;
- user.PasswordHash = HashPassword(newPassword);
- Save();
- return true;
- }
- }
-
- public bool DeleteUser(int id)
- {
- lock (_lock)
- {
- var user = _users.FirstOrDefault(u => u.Id == id);
- if (user == null) return false;
- _users.Remove(user);
- Save();
- return true;
- }
- }
-
- public bool VerifyPassword(string password, string hash)
- {
- return HashPassword(password) == hash;
- }
-
- private string HashPassword(string password)
- {
- var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(password + "CannedNetAdminSalt!@#$"));
- return Convert.ToHexString(bytes).ToLowerInvariant();
- }
-
- private void Load()
- {
- try
- {
- if (File.Exists(_filePath))
- {
- var json = File.ReadAllText(_filePath);
- var users = JsonSerializer.Deserialize>(json);
- if (users != null && users.Count > 0)
- {
- _users = users;
- _nextId = _users.Max(u => u.Id) + 1;
- }
- }
- }
- catch { }
- }
-
- private void Save()
- {
- try
- {
- var json = JsonSerializer.Serialize(_users, new JsonSerializerOptions { WriteIndented = true });
- File.WriteAllText(_filePath, json);
- }
- catch { }
- }
-}
diff --git a/CannedNet.Admin/Services/JwtBearerHandler.cs b/CannedNet.Admin/Services/JwtBearerHandler.cs
new file mode 100644
index 0000000..0bea00c
--- /dev/null
+++ b/CannedNet.Admin/Services/JwtBearerHandler.cs
@@ -0,0 +1,24 @@
+using System.Net.Http.Headers;
+using Microsoft.AspNetCore.Components.Authorization;
+
+namespace CannedNet.Admin.Services;
+
+public class JwtBearerHandler : DelegatingHandler
+{
+ private readonly AuthenticationStateProvider _authStateProvider;
+
+ public JwtBearerHandler(AuthenticationStateProvider authStateProvider)
+ {
+ _authStateProvider = authStateProvider;
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var state = await _authStateProvider.GetAuthenticationStateAsync();
+ var token = state.User.FindFirst("access_token")?.Value;
+ if (token != null)
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+
+ return await base.SendAsync(request, cancellationToken);
+ }
+}
diff --git a/CannedNet.Shared/Models/AccountData/Account.cs b/CannedNet.Shared/Models/AccountData/Account.cs
index 3d37962..e5c8553 100644
--- a/CannedNet.Shared/Models/AccountData/Account.cs
+++ b/CannedNet.Shared/Models/AccountData/Account.cs
@@ -13,4 +13,5 @@ public class Account
[JsonPropertyName("username")] public string? Username { get; set; }
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
[JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
+ [JsonPropertyName("password")] public string? Password { get; set; }
}
\ No newline at end of file
diff --git a/CannedNet/CannedNet.csproj b/CannedNet/CannedNet.csproj
index 3bf911c..a5e5526 100644
--- a/CannedNet/CannedNet.csproj
+++ b/CannedNet/CannedNet.csproj
@@ -14,7 +14,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/CannedNet/JSON/endpoints.json b/CannedNet/JSON/endpoints.json
index 080b2fd..b675a44 100644
--- a/CannedNet/JSON/endpoints.json
+++ b/CannedNet/JSON/endpoints.json
@@ -35,4 +35,4 @@
"Thorn": "https://cannednet.lapis.codes/thorn",
"Videos": "https://cannednet.lapis.codes/videos",
"WWW": "https://cannednet.lapis.codes"
-}
+}
\ No newline at end of file
diff --git a/CannedNet/Migrations/20260618140741_AddAccountPassword.Designer.cs b/CannedNet/Migrations/20260618140741_AddAccountPassword.Designer.cs
new file mode 100644
index 0000000..57f7101
--- /dev/null
+++ b/CannedNet/Migrations/20260618140741_AddAccountPassword.Designer.cs
@@ -0,0 +1,1137 @@
+//
+using System;
+using System.Collections.Generic;
+using CannedNet.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace CannedNet.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260618140741_AddAccountPassword")]
+ partial class AddAccountPassword
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CannedNet.Account", b =>
+ {
+ b.Property("AccountId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("accountId");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasJsonPropertyName("createdAt");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("displayName");
+
+ b.Property("IdentityFlags")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("identityFlags");
+
+ b.Property("IsJunior")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isJunior");
+
+ b.Property("Password")
+ .HasColumnType("text")
+ .HasJsonPropertyName("password");
+
+ b.Property("PersonalPronouns")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("personalPronouns");
+
+ b.Property("Platforms")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("platforms");
+
+ b.Property("ProfileImage")
+ .HasColumnType("text")
+ .HasJsonPropertyName("profileImage");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("username");
+
+ b.HasKey("AccountId");
+
+ b.ToTable("accounts", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.AvatarItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AvatarItemDesc")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("avatarItemDesc");
+
+ b.Property("AvatarItemType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("avatarItemType");
+
+ b.Property("FriendlyName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("friendlyName");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("PlatformMask")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("platformMask");
+
+ b.Property("Rarity")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("rarity");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("tooltip");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId");
+
+ b.ToTable("avatar_items", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.CachedLogin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AccountId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("accountId");
+
+ b.Property("LastLoginTime")
+ .HasColumnType("timestamp with time zone")
+ .HasJsonPropertyName("lastLoginTime");
+
+ b.Property("Platform")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("platform");
+
+ b.Property("PlatformID")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("platformId");
+
+ b.Property("RequirePassword")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("requirePassword");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Platform", "PlatformID");
+
+ b.ToTable("cached_logins", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.LoadScreen", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ImageUrl")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("imageUrl");
+
+ b.Property("IsThumbnail")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isThumbnail");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("tooltip");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("load_screens", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.ConsumableItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ActiveDurationMinutes")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("ActiveDurationMinutes");
+
+ b.Property("ConsumableItemDesc")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("ConsumableItemDesc");
+
+ b.Property("Count")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("Count");
+
+ b.PrimitiveCollection>("CreatedAts")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone[]")
+ .HasJsonPropertyName("CreatedAts");
+
+ b.PrimitiveCollection>("Ids")
+ .IsRequired()
+ .HasColumnType("integer[]")
+ .HasJsonPropertyName("Ids");
+
+ b.Property("InitialCount")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("InitialCount");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("IsActive");
+
+ b.Property("IsTransferable")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("IsTransferable");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId");
+
+ b.ToTable("consumable_items", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.EarnableReward", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AvatarItemDesc")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("avatarItemDesc");
+
+ b.Property("AvatarItemType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("avatarItemType");
+
+ b.Property("ConsumableItemDesc")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("consumableItemDesc");
+
+ b.Property("FriendlyName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("friendlyName");
+
+ b.Property("GiftRarity")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("giftRarity");
+
+ b.Property("RewardContext")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("rewardContext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RewardContext");
+
+ b.ToTable("earnable_rewards", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.GiftDrop", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AvatarItemDesc")
+ .HasColumnType("text");
+
+ b.Property("AvatarItemType")
+ .HasColumnType("integer");
+
+ b.Property("ConsumableItemDesc")
+ .HasColumnType("text");
+
+ b.Property("Context")
+ .HasColumnType("integer");
+
+ b.Property("Currency")
+ .HasColumnType("integer");
+
+ b.Property("CurrencyType")
+ .HasColumnType("integer");
+
+ b.Property("EquipmentModificationGuid")
+ .HasColumnType("text");
+
+ b.Property("EquipmentPrefabName")
+ .HasColumnType("text");
+
+ b.Property("FriendlyName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GiftDropId")
+ .HasColumnType("integer");
+
+ b.Property("IsQuery")
+ .HasColumnType("boolean");
+
+ b.Property("ItemSetFriendlyName")
+ .HasColumnType("text");
+
+ b.Property("ItemSetId")
+ .HasColumnType("integer");
+
+ b.Property("Level")
+ .HasColumnType("integer");
+
+ b.Property("Rarity")
+ .HasColumnType("integer");
+
+ b.Property("StorefrontItemId")
+ .HasColumnType("integer");
+
+ b.Property("SubscribersOnly")
+ .HasColumnType("boolean");
+
+ b.Property("Tooltip")
+ .HasColumnType("text");
+
+ b.Property("Unique")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.HasIndex("StorefrontItemId");
+
+ b.ToTable("gift_drops", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.ReceivedGift", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AvatarItemDesc")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("avatarItemDesc");
+
+ b.Property("AvatarItemType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("avatarItemType");
+
+ b.Property("BalanceType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("balanceType");
+
+ b.Property("ConsumableItemDesc")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("consumableItemDesc");
+
+ b.Property("ConsumedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasJsonPropertyName("consumedAt");
+
+ b.Property("Currency")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("currency");
+
+ b.Property("CurrencyType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("currencyType");
+
+ b.Property("EquipmentModificationGuid")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("equipmentModificationGuid");
+
+ b.Property("EquipmentPrefabName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("equipmentPrefabName");
+
+ b.Property("FriendlyName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("friendlyName");
+
+ b.Property("FromPlayerId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("fromPlayerId");
+
+ b.Property("GiftContext")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("giftContext");
+
+ b.Property("GiftRarity")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("giftRarity");
+
+ b.Property("IsConsumed")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isConsumed");
+
+ b.Property("Level")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("level");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("message");
+
+ b.Property("Platform")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("platform");
+
+ b.Property("PlatformsToSpawnOn")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("platformsToSpawnOn");
+
+ b.Property("ReceivedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasJsonPropertyName("receivedAt");
+
+ b.Property("ReceiverAccountId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("receiverAccountId");
+
+ b.Property("Xp")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("xp");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ReceiverAccountId");
+
+ b.HasIndex("ReceiverAccountId", "IsConsumed");
+
+ b.ToTable("received_gifts", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.Storefront", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NextUpdate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("StorefrontType")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("storefronts", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.StorefrontItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IsFeatured")
+ .HasColumnType("boolean");
+
+ b.Property("NewUntil")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PurchasableItemId")
+ .HasColumnType("integer");
+
+ b.Property("StorefrontId")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("StorefrontId");
+
+ b.ToTable("storefront_items", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.StorefrontPrice", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CurrencyType")
+ .HasColumnType("integer");
+
+ b.Property("Price")
+ .HasColumnType("integer");
+
+ b.Property("StorefrontItemId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("StorefrontItemId");
+
+ b.ToTable("storefront_prices", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PlayerAvatar", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FaceFeatures")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("FaceFeatures");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("HairColor");
+
+ b.Property("OutfitSelections")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("OutfitSelections");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("SkinColor");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId")
+ .IsUnique();
+
+ b.ToTable("player_avatars", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PlayerBio", b =>
+ {
+ b.Property("accountId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasJsonPropertyName("accountId");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("accountId"));
+
+ b.HasKey("accountId");
+
+ b.ToTable("player_bios", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PlayerProgression", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Level")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("level");
+
+ b.Property("PlayerId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("playerId");
+
+ b.Property("Xp")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("xp");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PlayerId")
+ .IsUnique();
+
+ b.ToTable("player_progressions", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PlayerSetting", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("key");
+
+ b.Property("PlayerId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("playerId");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("value");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PlayerId", "Key")
+ .IsUnique();
+
+ b.ToTable("player_settings", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PromoExternalContent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("tooltip");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("type");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("url");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("promo_external_contents", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PromoImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ImageUrl")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("imageUrl");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("sortOrder");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("tooltip");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("promo_images", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Room", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer");
+
+ b.Property("Accessibility")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("accessibility");
+
+ b.Property("CloningAllowed")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("cloningAllowed");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasJsonPropertyName("createdAt");
+
+ b.Property("CreatorAccountId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("creatorAccountId");
+
+ b.Property("CustomWarning")
+ .HasColumnType("text")
+ .HasJsonPropertyName("customWarning");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("description");
+
+ b.Property("DisableMicAutoMute")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("disableMicAutoMute");
+
+ b.Property("DisableRoomComments")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("disableRoomComments");
+
+ b.Property("EncryptVoiceChat")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("encryptVoiceChat");
+
+ b.Property("ImageName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("imageName");
+
+ b.Property("IsDorm")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isDorm");
+
+ b.Property("IsRRO")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isRRO");
+
+ b.Property("MinLevel")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("minLevel");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("name");
+
+ b.Property("RoomId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("roomId");
+
+ b.Property("State")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("state");
+
+ b.Property("SupportsJuniors")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsJuniors");
+
+ b.Property("SupportsLevelVoting")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsLevelVoting");
+
+ b.Property("SupportsMobile")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsMobile");
+
+ b.Property("SupportsQuest2")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsQuest2");
+
+ b.Property("SupportsScreens")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsScreens");
+
+ b.Property("SupportsTeleportVR")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsTeleportVR");
+
+ b.Property("SupportsVRLow")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsVRLow");
+
+ b.Property("SupportsWalkVR")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("supportsWalkVR");
+
+ b.Property("Tags")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("tags");
+
+ b.Property("WarningMask")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("warningMask");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatorAccountId");
+
+ b.ToTable("rooms", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.RoomInstance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("EncryptVoiceChat")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("EncryptVoiceChat");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("clubId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("clubId");
+
+ b.Property("dataBlob")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("dataBlob");
+
+ b.Property("eventId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("eventId");
+
+ b.Property("isFull")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isFull");
+
+ b.Property("isInProgress")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isInProgress");
+
+ b.Property("isPrivate")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isPrivate");
+
+ b.Property("location")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("location");
+
+ b.Property("maxCapacity")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("maxCapacity");
+
+ b.Property("name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("name");
+
+ b.Property("photonRegionId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("photonRegionId");
+
+ b.Property("photonRoomId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("photonRoomId");
+
+ b.Property("roomCode")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("roomCode");
+
+ b.Property("roomId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("roomId");
+
+ b.Property("roomInstanceId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("roomInstanceId");
+
+ b.Property("roomInstanceType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("roomInstanceType");
+
+ b.Property("subRoomId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("subRoomId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId", "roomId");
+
+ b.ToTable("room_instances", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.RoomRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AccountId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("accountId");
+
+ b.Property("InvitedRole")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("invitedRole");
+
+ b.Property("Role")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("role");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("room_roles", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.SavedOutfit", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FaceFeatures")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("FaceFeatures");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("HairColor");
+
+ b.Property("OutfitSelections")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("OutfitSelections");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("PreviewImageName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("PreviewImageName");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("SkinColor");
+
+ b.Property("Slot")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("Slot");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId")
+ .IsUnique();
+
+ b.ToTable("saved_outfits", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.SubRoom", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Accessibility")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("accessibility");
+
+ b.Property("DataBlob")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("dataBlob");
+
+ b.Property("DataSavedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasJsonPropertyName("dataSavedAt");
+
+ b.Property("IsSandbox")
+ .HasColumnType("boolean")
+ .HasJsonPropertyName("isSandbox");
+
+ b.Property("MaxPlayers")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("maxPlayers");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("name");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("SubRoomId")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("subRoomId");
+
+ b.Property("UnitySceneId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasJsonPropertyName("unitySceneId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("sub_rooms", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.TokenBalance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Balance")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("Balance");
+
+ b.Property("BalanceType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("BalanceType");
+
+ b.Property("CurrencyType")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("CurrencyType");
+
+ b.Property("Platform")
+ .HasColumnType("integer")
+ .HasJsonPropertyName("Platform");
+
+ b.HasKey("Id");
+
+ b.ToTable("token_balances", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Models.GiftDrop", b =>
+ {
+ b.HasOne("CannedNet.Models.StorefrontItem", "StorefrontItem")
+ .WithMany("GiftDrops")
+ .HasForeignKey("StorefrontItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("StorefrontItem");
+ });
+
+ modelBuilder.Entity("CannedNet.Models.StorefrontItem", b =>
+ {
+ b.HasOne("CannedNet.Models.Storefront", "Storefront")
+ .WithMany("Items")
+ .HasForeignKey("StorefrontId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Storefront");
+ });
+
+ modelBuilder.Entity("CannedNet.Models.StorefrontPrice", b =>
+ {
+ b.HasOne("CannedNet.Models.StorefrontItem", "StorefrontItem")
+ .WithMany("Prices")
+ .HasForeignKey("StorefrontItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("StorefrontItem");
+ });
+
+ modelBuilder.Entity("CannedNet.Models.Storefront", b =>
+ {
+ b.Navigation("Items");
+ });
+
+ modelBuilder.Entity("CannedNet.Models.StorefrontItem", b =>
+ {
+ b.Navigation("GiftDrops");
+
+ b.Navigation("Prices");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/CannedNet/Migrations/20260618140741_AddAccountPassword.cs b/CannedNet/Migrations/20260618140741_AddAccountPassword.cs
new file mode 100644
index 0000000..b6a4242
--- /dev/null
+++ b/CannedNet/Migrations/20260618140741_AddAccountPassword.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CannedNet.Migrations
+{
+ ///
+ public partial class AddAccountPassword : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Password",
+ table: "accounts",
+ type: "text",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Password",
+ table: "accounts");
+ }
+ }
+}
diff --git a/CannedNet/Migrations/AppDbContextModelSnapshot.cs b/CannedNet/Migrations/AppDbContextModelSnapshot.cs
index 0feff5a..f8b7e85 100644
--- a/CannedNet/Migrations/AppDbContextModelSnapshot.cs
+++ b/CannedNet/Migrations/AppDbContextModelSnapshot.cs
@@ -18,7 +18,7 @@ namespace CannedNet.Migrations
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "10.0.8")
+ .HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -46,6 +46,10 @@ namespace CannedNet.Migrations
.HasColumnType("boolean")
.HasJsonPropertyName("isJunior");
+ b.Property("Password")
+ .HasColumnType("text")
+ .HasJsonPropertyName("password");
+
b.Property("PersonalPronouns")
.HasColumnType("integer")
.HasJsonPropertyName("personalPronouns");
diff --git a/CannedNet/Program.cs b/CannedNet/Program.cs
index fed7514..be3145a 100644
--- a/CannedNet/Program.cs
+++ b/CannedNet/Program.cs
@@ -39,6 +39,7 @@ public static class Program
builder.Services.AddSingleton