diff --git a/.gitignore b/.gitignore index 855fc64..6f6e10f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ CannedNet/Data/PrivateKey.pem /CannedNet/.zed CannedNet/Data/PublicKey.pem CannedNet.Admin/admin_users.json +CannedNet/config.json diff --git a/CannedNet.Admin/Components/Layout/MainLayout.razor b/CannedNet.Admin/Components/Layout/MainLayout.razor index 85433b4..35050b5 100644 --- a/CannedNet.Admin/Components/Layout/MainLayout.razor +++ b/CannedNet.Admin/Components/Layout/MainLayout.razor @@ -1,7 +1,4 @@ -@using CannedNet.Admin.Services -@inherits LayoutComponentBase -@inject AdminAuthService AuthService -@inject NavigationManager Navigation +@inherits LayoutComponentBase @{ var body = Body; diff --git a/CannedNet.Admin/Components/Layout/NavMenu.razor b/CannedNet.Admin/Components/Layout/NavMenu.razor index 359e2fd..47eb626 100644 --- a/CannedNet.Admin/Components/Layout/NavMenu.razor +++ b/CannedNet.Admin/Components/Layout/NavMenu.razor @@ -16,18 +16,6 @@ - - - - - - @code { - [CascadingParameter] private Task? AuthState { get; set; } - private string currentPassword = ""; private string newPassword = ""; private string confirmNewPassword = ""; @@ -75,17 +71,15 @@ return; } - var state = await AuthState!; - var userIdClaim = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (userIdClaim == null || !int.TryParse(userIdClaim, out var userId)) + var content = new FormUrlEncodedContent(new Dictionary { - message = "Error: Could not identify user."; - return; - } + ["newPassword"] = newPassword + }); - if (!AuthService.ChangePassword(userId, currentPassword, newPassword)) + var response = await Http.PutAsync("/accounts/me/changepassword", content); + if (!response.IsSuccessStatusCode) { - message = "Error: Current password is incorrect."; + message = "Error: Failed to change password."; return; } diff --git a/CannedNet.Admin/Components/Pages/Config/NameserverConfig.razor b/CannedNet.Admin/Components/Pages/Config/NameserverConfig.razor deleted file mode 100644 index 40b0529..0000000 --- a/CannedNet.Admin/Components/Pages/Config/NameserverConfig.razor +++ /dev/null @@ -1,115 +0,0 @@ -@page "/config/nameserver" -@rendermode InteractiveServer -@using CannedNet.Admin.Models -@using System.Text.Json - -NS Config - -

Nameserver (NS) Config

-

Manage the nameserver endpoints.json file that controls where the game client connects.

- -
- This is the same editor as the Setup > Nameserver page. - All endpoint URLs are derived from a single base URL. -
- -@if (loading) -{ -
-} -else if (!string.IsNullOrEmpty(error)) -{ -
@error
-} -else -{ -
-
- - @if (!string.IsNullOrEmpty(saveMessage)) - { - @saveMessage - } -
- -
- - -
All endpoint URLs will use this as the base.
-
- -
- - - - - - - - - @foreach (var (name, path) in EndpointDefinitions.Endpoints) - { - - - - - } - -
ServiceFull URL
@name@(baseUrl.TrimEnd('/') + path)
-
-
-} - -@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/Components/Pages/Home.razor b/CannedNet.Admin/Components/Pages/Home.razor index c34f18c..b88063b 100644 --- a/CannedNet.Admin/Components/Pages/Home.razor +++ b/CannedNet.Admin/Components/Pages/Home.razor @@ -8,15 +8,6 @@

Manage your CannedNet Rec Room private server instance.

-
-
-
-
Setup
-

Configure nameserver endpoints, admin accounts, and authentication.

- Go to Setup -
-
-
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 -
+
+
+
+

Admin Login

+
+
+ @if (errorMessage != null) + { +
@errorMessage
+ } +
+ + +
+
+ + +
+ +
-} -else -{ -
-
-
-

Admin Login

-
-
- @if (errorMessage != null) - { -
@errorMessage
- } -
- - -
-
- - -
- -
-
-
-
-
-} +
@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.

- -
-
-
-
-
Room Details
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- -
-
-
SubRoom
-
-
- - -
- - @if (createSubRoom) - { -
- - -
-
- - -
- } -
-
- -
-
Platform Support
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- - - Cancel - - @if (!string.IsNullOrEmpty(message)) - { -
@message
- } - - @if (createdRoom != null) - { -
- Room created! Edit Room #@createdRoom.RoomId -
- } -
- -@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.

- -
-
Create New Admin Account
-
-
-
-
- - -
-
- - -
-
- - -
-
- - @if (!string.IsNullOrEmpty(createMessage)) - { - @createMessage - } -
-
-
- -
-
Admin Accounts
-
- - - - - - - - - - - - @foreach (var user in AuthService.GetAll()) - { - - - - - } - else - { - - } - - - - - } - -
IDUsernameRoleCreatedActions
@user.Id@user.Username - @if (user.IsOwner) - { - OwnerAdmin@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 - -
-
-
-

First-Time Setup

-
- @if (completed) - { -
-
Owner account created!
-

You can now log in with your new admin account.

-
- } - else if (AuthService.HasAnyUser()) - { -
- Admin accounts already exist. Go to login. -
- } - else - { -

Create the initial owner account. This account will have full access and can create additional admin accounts.

-
-
- - -
-
- - -
-
- - -
- - @if (!string.IsNullOrEmpty(message)) - { -
@message
- } -
- } -
-
-
-
- -@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 -{ -
-
- - @if (!string.IsNullOrEmpty(saveMessage)) - { - @saveMessage - } -
- -
- - -
All endpoint URLs will use this as the base.
-
- -
- - - - - - - - - @foreach (var (name, path) in EndpointDefinitions.Endpoints) - { - - - - - } - -
ServiceFull URL
@name@(baseUrl.TrimEnd('/') + path)
-
-
-} - -@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(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddSignalR(); builder.Services.AddAuthentication(options => @@ -70,7 +71,7 @@ public static class Program ClockSkew = TimeSpan.Zero, NameClaimType = ClaimTypes.NameIdentifier, - RoleClaimType = "role" + RoleClaimType = ClaimTypes.Role }; }) .AddScheme("AdminApiKey", null); @@ -89,14 +90,15 @@ public static class Program IHubContext hubContext = app.Services.GetRequiredService>(); NotificationService.SetHubContext(hubContext); - //if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(options => + if (app.Environment.IsDevelopment()) { - options.SwaggerEndpoint("/swagger/v1/swagger.json", "CannedNet v1"); - options.RoutePrefix = "swagger/ui/"; - }); - //} + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "CannedNet v1"); + options.RoutePrefix = "swagger/ui/"; + }); + } using IServiceScope scope = app.Services.CreateScope(); AppDbContext db = scope.ServiceProvider.GetRequiredService(); @@ -117,50 +119,4 @@ public static class Program await app.RunAsync(); } - - //public static async Task T() { - // List<(WebApplication App, ServiceRegistry Service)> apps = new(); - // - // foreach (var service in Services.Infrastructure.Services.All) - // { - // var builder = WebApplication.CreateBuilder(); - // - // builder.Services.AddLogging(logging => - // { - // logging.AddConsole(); - // logging.SetMinimumLevel(LogLevel.Debug); - // }); - // - // service.ConfigureBuilder(builder); - // builder.WebHost.UseUrls($"http://*:{service.Port}"); - // builder.Configuration.AddJsonFile($"AppConfigs/appsettings.{service.Name}.json", optional: true, reloadOnChange: true); - // - // apps.Add((builder.Build(), service)); - // } - // - // using var scope = apps[0].App.Services.CreateScope(); - // var dbContext = scope.ServiceProvider.GetRequiredService(); - // try - // { - // dbContext.Database.Migrate(); - // } - // catch (Exception ex) - // { - // dbContext.Database.EnsureCreated(); - // } - // - // // automatically fill out storefronts tables from the JSON's - // var seedingService = scope.ServiceProvider.GetRequiredService(); - // await seedingService.FillStorefrontsAsync(); - // - // var jwtService = scope.ServiceProvider.GetRequiredService(); - // - // foreach (var (app, service) in apps) - // { - // service.MapEndpoints(app, jwtService); - // app.UseRequestLogging(); - // } - // - // await Task.WhenAll(apps.Select(t => t.App.RunAsync())); - //} } diff --git a/CannedNet/Services/ConfigService.cs b/CannedNet/Services/ConfigService.cs new file mode 100644 index 0000000..1621f10 --- /dev/null +++ b/CannedNet/Services/ConfigService.cs @@ -0,0 +1,117 @@ +using System.Text.Json; + +namespace CannedNet.Services; + +public class ConfigService +{ + private const string ConfigPath = "config.json"; + private const string EndpointsPath = "JSON/endpoints.json"; + private static readonly JsonSerializerOptions Indented = new() { WriteIndented = true }; + + public AppConfig Config { get; private set; } + + public ConfigService() + { + LoadConfig(); + SyncBaseUrlToEndpoints(); + } + + private void LoadConfig() + { + if (!File.Exists(ConfigPath)) + { + Config = CreateDefaultConfig(); + File.WriteAllText(ConfigPath, JsonSerializer.Serialize(Config, Indented)); + } + else + { + var json = File.ReadAllText(ConfigPath); + Config = JsonSerializer.Deserialize(json) ?? CreateDefaultConfig(); + } + } + + public void SaveConfig(AppConfig updated) + { + Config = updated; + File.WriteAllText(ConfigPath, JsonSerializer.Serialize(Config, Indented)); + SyncBaseUrlToEndpoints(); + } + + private void SyncBaseUrlToEndpoints() + { + var configBase = Config.BaseUrl?.TrimEnd('/'); + if (string.IsNullOrEmpty(configBase)) + return; + + if (!File.Exists(EndpointsPath)) + return; + + var endpointsJson = File.ReadAllText(EndpointsPath); + var endpoints = JsonSerializer.Deserialize>(endpointsJson); + if (endpoints == null || endpoints.Count == 0) + return; + + var currentBase = DetectBaseUrl(endpoints); + if (currentBase == null) + return; + + if (string.Equals(currentBase, configBase, StringComparison.OrdinalIgnoreCase)) + return; + + var updated = new Dictionary(endpoints.Count); + foreach (var (key, url) in endpoints) + { + if (url.StartsWith(currentBase, StringComparison.OrdinalIgnoreCase)) + { + var path = url[currentBase.Length..]; + updated[key] = configBase + path; + } + else + { + updated[key] = url; + } + } + + File.WriteAllText(EndpointsPath, JsonSerializer.Serialize(updated, Indented)); + } + + private static string? DetectBaseUrl(Dictionary endpoints) + { + foreach (var url in endpoints.Values) + { + if (string.IsNullOrEmpty(url)) + continue; + if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) + return $"{uri.Scheme}://{uri.Host}{(uri.IsDefaultPort ? "" : $":{uri.Port}")}"; + } + return null; + } + + private static AppConfig CreateDefaultConfig() + { + return new AppConfig + { + BaseUrl = "http://localhost:5000", + DeveloperUserIds = new List(), + VModUserIds = new List(), + AutoFillRROs = true, + AutoFillStorefronts = true, + WhitelistOn = false, + WhitelistedPlatformIds = new List(), + EnableAccountCreation = true, + }; + } + + public class AppConfig + { + public string BaseUrl { get; set; } = ""; + public bool AutoFillRROs { get; set; } = true; + public bool AutoFillStorefronts { get; set; } = true; + public List DeveloperUserIds { get; set; } = new(); + public List VModUserIds { get; set; } = new(); + public List AdminAccountIds { get; set; } = new(); + public bool WhitelistOn { get; set; } = false; + public List WhitelistedPlatformIds { get; set; } = new(); + public bool EnableAccountCreation { get; set; } = true; + } +} diff --git a/CannedNet/Services/Controllers/AccountsController.cs b/CannedNet/Services/Controllers/AccountsController.cs index a5514be..2aa4631 100644 --- a/CannedNet/Services/Controllers/AccountsController.cs +++ b/CannedNet/Services/Controllers/AccountsController.cs @@ -1,6 +1,7 @@ using CannedNet.Data; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; @@ -100,132 +101,6 @@ public class AccountsController : ControllerBase return Results.Json(result); } - [HttpPost("account/create")] - public async Task Create(AppDbContext db) - { - var request = HttpContext.Request; - int platform = 0; - string platformId = ""; - - if (request.ContentLength is > 0) - { - try - { - string contentType = request.ContentType ?? ""; - request.EnableBuffering(); - - using StreamReader reader = new(request.Body, leaveOpen: true); - string body = await reader.ReadToEndAsync(); - - if (!string.IsNullOrWhiteSpace(body) && contentType.Contains("application/x-www-form-urlencoded")) - { - foreach (string pair in body.Split('&')) - { - string[] keyValue = pair.Split('='); - if (keyValue.Length != 2) continue; - string key = Uri.UnescapeDataString(keyValue[0]); - string value = Uri.UnescapeDataString(keyValue[1]); - - switch (key) - { - case "platform" when int.TryParse(value, out var parsedPlatform): - platform = parsedPlatform; - break; - case "platformId": - platformId = value; - break; - } - } - } - - request.Body.Position = 0; - } - catch { } - } - - int accountId = new Random().Next(10000, 99999); - Account account = new() - { - AccountId = accountId, - ProfileImage = "DefaultProfileImage.jpg", - IsJunior = false, - Platforms = 0, - PersonalPronouns = 0, - IdentityFlags = 0, - Username = $"Player{accountId}", - DisplayName = $"Player{accountId}", - CreatedAt = DateTime.UtcNow - }; - - db.Accounts.Add(account); - - if (!string.IsNullOrEmpty(platformId)) - { - db.CachedLogins.Add(new CachedLogin - { - AccountId = accountId, - Platform = (PlatformType)platform, - PlatformID = platformId, - LastLoginTime = DateTime.UtcNow, - RequirePassword = false - }); - } - - await db.SaveChangesAsync(); - - int maxRoomId = await db.Rooms.MaxAsync(r => (int?)r.RoomId) ?? 0; - int maxId = await db.Rooms.MaxAsync(r => (int?)r.Id) ?? 0; - int dormRoomId = maxRoomId + 1; - Room dormRoom = new Room - { - Id = maxId + 1, - RoomId = dormRoomId, - Name = "DormRoom", - Description = "Your personal room", - CreatorAccountId = accountId, - ImageName = "", - State = 0, - Accessibility = 0, - SupportsLevelVoting = false, - IsRRO = false, - IsDorm = true, - CloningAllowed = false, - SupportsVRLow = true, - SupportsQuest2 = true, - SupportsMobile = true, - SupportsScreens = true, - SupportsWalkVR = true, - SupportsTeleportVR = true, - SupportsJuniors = true, - MinLevel = 0, - WarningMask = 0, - CustomWarning = null, - DisableMicAutoMute = false, - DisableRoomComments = false, - EncryptVoiceChat = false, - CreatedAt = DateTime.UtcNow, - Tags = "[]" - }; - db.Rooms.Add(dormRoom); - - SubRoom dormSubRoom = new SubRoom - { - RoomId = dormRoomId, - SubRoomId = 1, - Name = "DormRoom", - DataBlob = "", - IsSandbox = false, - MaxPlayers = 4, - Accessibility = 0, - UnitySceneId = "76d98498-60a1-430c-ab76-b54a29b7a163", - DataSavedAt = DateTime.UtcNow - }; - db.SubRooms.Add(dormSubRoom); - - await db.SaveChangesAsync(); - return Results.Ok(RecNetResult.Ok(account)); - } - [HttpPut("me/displayname")] [Authorize] public async Task PutMeDisplayName(AppDbContext db) @@ -320,6 +195,54 @@ public class AccountsController : ControllerBase return Results.Ok(RecNetResult.Ok()); } + [HttpPut("me/changepassword")] + [Authorize] + public async Task PutMePassword(AppDbContext db) + { + if (!int.TryParse(User.Identity?.Name, out var id)) + return Results.Unauthorized(); + + var request = HttpContext.Request; + string newPassword = ""; + + if (request.ContentLength is > 0) + { + try + { + request.EnableBuffering(); + using StreamReader reader = new(request.Body, leaveOpen: true); + string body = await reader.ReadToEndAsync(); + + if (!string.IsNullOrWhiteSpace(body)) + { + foreach (string pair in body.Split('&')) + { + string[] keyValue = pair.Split('='); + if (keyValue.Length != 2) continue; + string key = Uri.UnescapeDataString(keyValue[0]); + string value = Uri.UnescapeDataString(keyValue[1]); + + if (key == "newPassword") + newPassword = value; + } + } + + request.Body.Position = 0; + } + catch { } + } + + Account? account = await db.Accounts.FindAsync(id); + if (account == null) + return Results.NotFound(); + + var hasher = new PasswordHasher(); + account.Password = hasher.HashPassword(account, newPassword); + await db.SaveChangesAsync(); + + return Results.Ok(RecNetResult.Ok()); + } + [HttpGet("{id}/bio")] public async Task GetBio(string id, AppDbContext db) { diff --git a/CannedNet/Services/Controllers/AdminController.cs b/CannedNet/Services/Controllers/AdminController.cs index b624915..37de518 100644 --- a/CannedNet/Services/Controllers/AdminController.cs +++ b/CannedNet/Services/Controllers/AdminController.cs @@ -8,27 +8,11 @@ using Microsoft.EntityFrameworkCore; namespace CannedNet.Services.Controllers; [ApiController, Route("admin/api")] -[Authorize(AuthenticationSchemes = "AdminApiKey")] +[Authorize(Roles = "developer,admin")] public class AdminController : ControllerBase { private const string JsonDir = "JSON"; - [HttpGet("endpoints")] - public async Task GetEndpoints() - { - var json = await System.IO.File.ReadAllTextAsync(Path.Combine(JsonDir, "endpoints.json")); - return Results.Content(json, "application/json"); - } - - [HttpPut("endpoints")] - public async Task SaveEndpoints() - { - using var reader = new StreamReader(Request.Body); - var body = await reader.ReadToEndAsync(); - await System.IO.File.WriteAllTextAsync(Path.Combine(JsonDir, "endpoints.json"), body); - return Results.Ok(new { success = true }); - } - [HttpGet("accounts/search")] public async Task SearchAccounts([FromQuery] string q, AppDbContext db) { @@ -215,98 +199,6 @@ public class AdminController : ControllerBase return Results.Ok(new { success = true }); } - [HttpPost("rooms")] - public async Task CreateRoom(AppDbContext db) - { - using var reader = new StreamReader(Request.Body); - var body = await reader.ReadToEndAsync(); - var data = JsonSerializer.Deserialize(body); - - int maxRoomId = await db.Rooms.MaxAsync(r => (int?)r.RoomId) ?? 0; - int maxId = await db.Rooms.MaxAsync(r => (int?)r.Id) ?? 0; - int newRoomId = data.TryGetProperty("roomId", out var rid) ? rid.GetInt32() : maxRoomId + 1; - - var room = new Room - { - Id = maxId + 1, - RoomId = newRoomId, - Name = data.TryGetProperty("name", out var name) ? name.GetString() ?? "New Room" : "New Room", - Description = data.TryGetProperty("description", out var desc) ? desc.GetString() ?? "" : "", - CreatorAccountId = data.TryGetProperty("creatorAccountId", out var creatorId) ? creatorId.GetInt32() : 0, - ImageName = data.TryGetProperty("imageName", out var img) ? img.GetString() ?? "" : "", - State = data.TryGetProperty("state", out var state) ? state.GetInt32() : 0, - Accessibility = data.TryGetProperty("accessibility", out var accessibility) ? accessibility.GetInt32() : 0, - IsDorm = false, - IsRRO = false, - CloningAllowed = true, - SupportsVRLow = true, - SupportsQuest2 = true, - SupportsMobile = true, - SupportsScreens = true, - SupportsWalkVR = true, - SupportsTeleportVR = true, - SupportsJuniors = true, - MinLevel = 0, - WarningMask = 0, - CreatedAt = DateTime.UtcNow, - Tags = "[]" - }; - - db.Rooms.Add(room); - await db.SaveChangesAsync(); - - // create default subroom - if (data.TryGetProperty("createSubRoom", out var createSub) && createSub.GetBoolean()) - { - var sub = new SubRoom - { - RoomId = newRoomId, - SubRoomId = 1, - Name = room.Name, - DataBlob = "", - IsSandbox = false, - MaxPlayers = 16, - Accessibility = 0, - // TODO: allow changing this scene id thru the dashboard - UnitySceneId = "00000000-0000-0000-0000-000000000000" - }; - db.SubRooms.Add(sub); - await db.SaveChangesAsync(); - } - - return Results.Ok(room); - } - - [HttpPost("rooms/{roomId:int}/subrooms")] - public async Task CreateSubRoom(int roomId, AppDbContext db) - { - var room = await db.Rooms.FirstOrDefaultAsync(r => r.RoomId == roomId); - if (room == null) - return Results.NotFound(); - - using var reader = new StreamReader(Request.Body); - var body = await reader.ReadToEndAsync(); - var data = JsonSerializer.Deserialize(body); - - int maxSubId = await db.SubRooms.Where(s => s.RoomId == roomId).MaxAsync(s => (int?)s.SubRoomId) ?? 0; - - var sub = new SubRoom - { - RoomId = roomId, - SubRoomId = maxSubId + 1, - Name = data.TryGetProperty("name", out var name) ? name.GetString() ?? "New SubRoom" : "New SubRoom", - DataBlob = data.TryGetProperty("dataBlob", out var dataBlob) ? dataBlob.GetString() ?? "" : "", - IsSandbox = data.TryGetProperty("isSandbox", out var isSandbox) && isSandbox.GetBoolean(), - MaxPlayers = data.TryGetProperty("maxPlayers", out var maxPlayers) ? maxPlayers.GetInt32() : 16, - Accessibility = data.TryGetProperty("accessibility", out var accessibility) ? accessibility.GetInt32() : 0, - UnitySceneId = data.TryGetProperty("unitySceneId", out var sceneId) ? sceneId.GetString() ?? "" : "", - }; - - db.SubRooms.Add(sub); - await db.SaveChangesAsync(); - return Results.Ok(sub); - } - [HttpGet("config/gameconfigs")] public async Task GetGameConfigs() { diff --git a/CannedNet/Services/Controllers/AuthController.cs b/CannedNet/Services/Controllers/AuthController.cs index ff39c2c..400d266 100644 --- a/CannedNet/Services/Controllers/AuthController.cs +++ b/CannedNet/Services/Controllers/AuthController.cs @@ -1,4 +1,5 @@ using CannedNet.Data; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; @@ -26,13 +27,16 @@ public class AuthController : ControllerBase } [HttpPost("connect/token")] - public async Task ConnectToken(AppDbContext db, JwtTokenService jwtService) + public async Task ConnectToken(AppDbContext db, JwtTokenService jwtService, ConfigService config) { string grantType = ""; string accountId = ""; string platformId = ""; string platform = ""; int platformInt = 0; + string password = ""; + string username = ""; + List adminRoles = []; if (HttpContext.Request.ContentLength is > 0) { @@ -61,11 +65,22 @@ public class AuthController : ControllerBase break; case "platform_id": platformId = value; + if (config.Config.WhitelistOn) + { + if (!config.Config.WhitelistedPlatformIds.Contains(platformId)) + return Results.Unauthorized(); + } break; case "platform" when int.TryParse(value, out var p): platformInt = p; platform = ((PlatformType)p).ToString(); break; + case "username": + username = value; + break; + case "password": + password = value; + break; } } } @@ -76,6 +91,11 @@ public class AuthController : ControllerBase if (grantType == "create_account") { + if (!config.Config.EnableAccountCreation) + { + return Results.Ok(RecNetResult.Err("Account creation is disabled")); + } + int newId = new Random().Next(10000, 99999); Account account = new() { @@ -159,8 +179,27 @@ public class AuthController : ControllerBase accountId = newId.ToString(); } + else if (grantType == "password") + { + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + return Results.Ok(RecNetResult.Err("Username and password are required")); - string accessToken = jwtService.GenerateToken(accountId, platformId, platform); + var account = await db.Accounts.FirstOrDefaultAsync(a => a.Username == username); + if (account == null || string.IsNullOrEmpty(account.Password)) + return Results.Ok(RecNetResult.Err("Invalid username or password")); + + var hasher = new PasswordHasher(); + var result = hasher.VerifyHashedPassword(account, account.Password, password); + if (result == PasswordVerificationResult.Failed) + return Results.Ok(RecNetResult.Err("Invalid username or password")); + + accountId = account.AccountId?.ToString() ?? ""; + if (account.AccountId != null && config.Config.AdminAccountIds.Contains(account.AccountId.Value.ToString())) + adminRoles.Add("developer"); + platformId = accountId; + } + + string accessToken = jwtService.GenerateToken(accountId, platformId, config, platform, grantType == "password" ? adminRoles : null, amr: grantType == "create_account" ? "create_account" : "cached_login"); if (!string.IsNullOrEmpty(accountId) && int.TryParse(accountId, out var id)) { diff --git a/CannedNet/Services/Controllers/MatchmakingController.cs b/CannedNet/Services/Controllers/MatchmakingController.cs index 64e7ea3..2f7cfec 100644 --- a/CannedNet/Services/Controllers/MatchmakingController.cs +++ b/CannedNet/Services/Controllers/MatchmakingController.cs @@ -421,9 +421,9 @@ public class MatchmakingController : ControllerBase return Results.Json(new { - playerId = heartbeat.playerId != 0 ? heartbeat.playerId : id, - statusVisibility = heartbeat.statusVisibility, - deviceClass = heartbeat.deviceClass, + PlayerId = heartbeat.playerId != 0 ? heartbeat.playerId : id, + StatusVisibility = heartbeat.statusVisibility, + DeviceClass = heartbeat.deviceClass, vrMovementMode = heartbeat.vrMovementMode != 0 ? heartbeat.vrMovementMode : 1, roomInstance = roomInstance != null ? new { @@ -445,9 +445,9 @@ public class MatchmakingController : ControllerBase isInProgress = roomInstance.isInProgress, EncryptVoiceChat = roomInstance.EncryptVoiceChat } : null, - isOnline = roomInstance != null, - appVersion = heartbeat.appVersion ?? "", - platform = heartbeat.platform + IsOnline = true, + AppVersion = heartbeat.appVersion ?? "", + Platform = heartbeat.platform }); } diff --git a/CannedNet/Services/Controllers/NSController.cs b/CannedNet/Services/Controllers/NSController.cs index 7ba3bb8..ab9958d 100644 --- a/CannedNet/Services/Controllers/NSController.cs +++ b/CannedNet/Services/Controllers/NSController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc; namespace CannedNet.Services.Controllers; -//TODO: proper NameServer configs [ApiController, Route("ns")] public class NSController : ControllerBase { diff --git a/CannedNet/Services/JwtTokenService.cs b/CannedNet/Services/JwtTokenService.cs index 44be3ed..58a367e 100644 --- a/CannedNet/Services/JwtTokenService.cs +++ b/CannedNet/Services/JwtTokenService.cs @@ -15,22 +15,22 @@ public class JwtTokenService public JwtTokenService() { - var rsa = Signatures.GetRsaInstance() + var rsa = Signatures.GetRsaInstance() ?? throw new InvalidOperationException("jwt - sigs service was not found"); _securityKey = new RsaSecurityKey(rsa) { KeyId = KeyId }; - + _signingCredentials = new SigningCredentials(_securityKey, SecurityAlgorithms.RsaSha256); } - public string GenerateToken(string accountId, string platformId, string platform = "Steam") + public string GenerateToken(string accountId, string platformId, ConfigService config, string platform = "Steam", List? additionalRoles = null, string amr = "cached_login") { var now = DateTime.UtcNow; var exp = now.AddHours(1); // theres gotta be some other better way to do this, but im fucking stupid so :shrug: // if it works it works ig idk, someone can clean this up later or i will later - + var claims = new List { new Claim("iss", "https://lapis.codes"), @@ -38,7 +38,7 @@ public class JwtTokenService new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()), new Claim("exp", ((DateTimeOffset)exp).ToUnixTimeSeconds().ToString()), new Claim("aud", "https://lapis.codes/resources"), - new Claim("amr", "cached_login"), + new Claim("amr", amr), new Claim("client_id", "recroom"), new Claim("sub", accountId), new Claim("auth_time", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()), @@ -48,11 +48,25 @@ public class JwtTokenService new Claim("rn.ver", "20210129"), new Claim("rn.plat", "0"), new Claim("role", "gameClient"), - new Claim("role", "developer"), - new Claim("role", "moderator"), - new Claim("jti", Guid.NewGuid().ToString("N").Substring(0, 32).ToUpper()) + new Claim("jti", Guid.NewGuid().ToString("N")[..32].ToUpper()) }; + if (config.Config.DeveloperUserIds.Contains(platformId)) + { + claims.Add(new Claim("role", "developer")); + } + + if (config.Config.VModUserIds.Contains(platformId)) + { + claims.Add(new Claim("role", "moderator")); + } + + if (additionalRoles != null) + { + foreach (var role in additionalRoles) + claims.Add(new Claim("role", role)); + } + claims.Add(new Claim("scope", "profile")); claims.Add(new Claim("scope", "rn")); claims.Add(new Claim("scope", "rn.accounts")); @@ -80,6 +94,8 @@ public class JwtTokenService expires: exp, signingCredentials: _signingCredentials); + token.Payload["amr"] = new List { amr }; + return handler.WriteToken(token); } @@ -89,7 +105,7 @@ public class JwtTokenService { var handler = new JwtSecurityTokenHandler(); handler.InboundClaimTypeMap.Clear(); - + var parameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, @@ -103,9 +119,9 @@ public class JwtTokenService }; var principal = handler.ValidateToken(token, parameters, out SecurityToken validatedToken); - + var accountIdClaim = principal.Claims.FirstOrDefault(c => c.Type == "sub"); - + return accountIdClaim?.Value; } catch (Exception ex) @@ -124,10 +140,10 @@ public class JwtTokenService var token = authHeader.Substring("Bearer ".Length); var accountId = ValidateAndGetAccountId(token); - + if (string.IsNullOrEmpty(accountId) || !int.TryParse(accountId, out var id)) return null; - + return id; } catch diff --git a/CannedNet/Services/StorefrontFillService.cs b/CannedNet/Services/StorefrontFillService.cs index c750844..4089d2b 100644 --- a/CannedNet/Services/StorefrontFillService.cs +++ b/CannedNet/Services/StorefrontFillService.cs @@ -10,12 +10,14 @@ public class StorefrontFillService private readonly AppDbContext _dbContext; private readonly ILogger _logger; private readonly IWebHostEnvironment _environment; + private readonly ConfigService _config; - public StorefrontFillService(AppDbContext dbContext, ILogger logger, IWebHostEnvironment environment) + public StorefrontFillService(AppDbContext dbContext, ILogger logger, IWebHostEnvironment environment, ConfigService config) { _dbContext = dbContext; _logger = logger; _environment = environment; + _config = config; } public async Task FillStorefrontsAsync() @@ -27,6 +29,11 @@ public class StorefrontFillService return; } + if (!_config.Config.AutoFillStorefronts) + { + return; + } + _logger.LogInformation("automatically filling out storefronts due to no data"); var storefronts = new[] @@ -81,7 +88,7 @@ public class StorefrontFillService { Name = name, StorefrontType = storefrontType, - NextUpdate = root.TryGetProperty("NextUpdate", out var nextUpdate) + NextUpdate = root.TryGetProperty("NextUpdate", out var nextUpdate) ? DateTime.SpecifyKind(DateTime.Parse(nextUpdate.GetString() ?? DateTime.UtcNow.ToString()), DateTimeKind.Utc) : DateTime.UtcNow.AddDays(7) }; diff --git a/CannedNet/appsettings.json b/CannedNet/appsettings.json index ae950fc..45b2d8d 100644 --- a/CannedNet/appsettings.json +++ b/CannedNet/appsettings.json @@ -11,9 +11,5 @@ "Ports": { "Primary": 5000 }, - "AllowedHosts": "*", - "Admin": { - "ApiKey": "changeme-replace-with-a-secure-key" - } + "AllowedHosts": "*" } -