mirror of
https://github.com/CannedNet/CannedNet.git
synced 2026-10-25 15:09:23 -07:00
@@ -10,3 +10,4 @@ CannedNet/Data/PrivateKey.pem
|
|||||||
/CannedNet/.zed
|
/CannedNet/.zed
|
||||||
CannedNet/Data/PublicKey.pem
|
CannedNet/Data/PublicKey.pem
|
||||||
CannedNet.Admin/admin_users.json
|
CannedNet.Admin/admin_users.json
|
||||||
|
CannedNet/config.json
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
@using CannedNet.Admin.Services
|
@inherits LayoutComponentBase
|
||||||
@inherits LayoutComponentBase
|
|
||||||
@inject AdminAuthService AuthService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var body = Body;
|
var body = Body;
|
||||||
|
|||||||
@@ -16,18 +16,6 @@
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-section-label px-3 mt-3 mb-1 text-muted small text-uppercase">Setup</div>
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="setup/nameserver">
|
|
||||||
<span class="bi bi-server-nav-menu" aria-hidden="true"></span> Nameserver
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="setup/accounts">
|
|
||||||
<span class="bi bi-people-fill-nav-menu" aria-hidden="true"></span> Admin Accounts
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-section-label px-3 mt-3 mb-1 text-muted small text-uppercase">Users</div>
|
<div class="nav-section-label px-3 mt-3 mb-1 text-muted small text-uppercase">Users</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="users/search">
|
<NavLink class="nav-link" href="users/search">
|
||||||
@@ -41,18 +29,8 @@
|
|||||||
<span class="bi bi-building-nav-menu" aria-hidden="true"></span> Search Rooms
|
<span class="bi bi-building-nav-menu" aria-hidden="true"></span> Search Rooms
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="rooms/create">
|
|
||||||
<span class="bi bi-plus-circle-fill-nav-menu" aria-hidden="true"></span> Create Room
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-section-label px-3 mt-3 mb-1 text-muted small text-uppercase">Config</div>
|
<div class="nav-section-label px-3 mt-3 mb-1 text-muted small text-uppercase">Config</div>
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="config/nameserver">
|
|
||||||
<span class="bi bi-diagram-3-fill-nav-menu" aria-hidden="true"></span> NS Config
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="config/gameconfig">
|
<NavLink class="nav-link" href="config/gameconfig">
|
||||||
<span class="bi bi-sliders-nav-menu" aria-hidden="true"></span> Game Config
|
<span class="bi bi-sliders-nav-menu" aria-hidden="true"></span> Game Config
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
@using CannedNet.Admin.Services
|
|
||||||
@inject AdminAuthService AuthService
|
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
if (!AuthService.HasAnyUser())
|
Navigation.NavigateTo("/login", forceLoad: true);
|
||||||
Navigation.NavigateTo("/setup", forceLoad: true);
|
|
||||||
else
|
|
||||||
Navigation.NavigateTo("/login", forceLoad: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
@page "/change-password"
|
@page "/change-password"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@using System.Security.Claims
|
@inject HttpClient Http
|
||||||
@using CannedNet.Admin.Services
|
|
||||||
@inject AdminAuthService AuthService
|
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
@@ -44,8 +42,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
|
||||||
|
|
||||||
private string currentPassword = "";
|
private string currentPassword = "";
|
||||||
private string newPassword = "";
|
private string newPassword = "";
|
||||||
private string confirmNewPassword = "";
|
private string confirmNewPassword = "";
|
||||||
@@ -75,17 +71,15 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await AuthState!;
|
var content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
var userIdClaim = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
if (userIdClaim == null || !int.TryParse(userIdClaim, out var userId))
|
|
||||||
{
|
{
|
||||||
message = "Error: Could not identify user.";
|
["newPassword"] = newPassword
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
@page "/config/nameserver"
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
@using CannedNet.Admin.Models
|
|
||||||
@using System.Text.Json
|
|
||||||
|
|
||||||
<PageTitle>NS Config</PageTitle>
|
|
||||||
|
|
||||||
<h3>Nameserver (NS) Config</h3>
|
|
||||||
<p class="text-muted">Manage the nameserver endpoints.json file that controls where the game client connects.</p>
|
|
||||||
|
|
||||||
<div class="alert alert-info">
|
|
||||||
This is the same editor as the <a href="setup/nameserver">Setup > Nameserver</a> page.
|
|
||||||
All endpoint URLs are derived from a single base URL.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (loading)
|
|
||||||
{
|
|
||||||
<div class="spinner-border" role="status"></div>
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(error))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">@error</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<form>
|
|
||||||
<div class="mb-3">
|
|
||||||
<button type="submit" class="btn btn-success" @onclick="Save" disabled="@saving">
|
|
||||||
@(saving ? "Saving..." : "Save Changes")
|
|
||||||
</button>
|
|
||||||
@if (!string.IsNullOrEmpty(saveMessage))
|
|
||||||
{
|
|
||||||
<span class="@(saveError ? "text-danger" : "text-success") ms-2">@saveMessage</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="baseUrl">Base URL:</label>
|
|
||||||
<input id="baseUrl" class="form-control font-monospace" @bind="baseUrl" placeholder="https://cannednet.lapis.codes" />
|
|
||||||
<div class="form-text">All endpoint URLs will use this as the base.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Service</th>
|
|
||||||
<th>Full URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var (name, path) in EndpointDefinitions.Endpoints)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td><code>@name</code></td>
|
|
||||||
<td><code>@(baseUrl.TrimEnd('/') + path)</code></td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,6 @@
|
|||||||
<p class="lead text-muted">Manage your CannedNet Rec Room private server instance.</p>
|
<p class="lead text-muted">Manage your CannedNet Rec Room private server instance.</p>
|
||||||
|
|
||||||
<div class="row g-4 mt-3">
|
<div class="row g-4 mt-3">
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Setup</h5>
|
|
||||||
<p class="card-text">Configure nameserver endpoints, admin accounts, and authentication.</p>
|
|
||||||
<a href="setup/nameserver" class="btn btn-primary">Go to Setup</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|||||||
@@ -1,52 +1,33 @@
|
|||||||
@page "/login"
|
@page "/login"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@using CannedNet.Admin.Services
|
|
||||||
@inject AdminAuthService AuthService
|
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<PageTitle>Login</PageTitle>
|
<PageTitle>Login</PageTitle>
|
||||||
|
|
||||||
@if (AuthService.HasAnyUser() == false)
|
<div class="row justify-content-center mt-5">
|
||||||
{
|
<div class="col-md-4">
|
||||||
<div class="row justify-content-center mt-5">
|
<div class="card">
|
||||||
<div class="col-md-4">
|
<div class="card-header"><h4 class="mb-0">Admin Login</h4></div>
|
||||||
<div class="card">
|
<div class="card-body">
|
||||||
<div class="card-body text-center">
|
<form method="post" action="/admin/login">
|
||||||
<h4>No Admin Accounts</h4>
|
@if (errorMessage != null)
|
||||||
<p class="text-muted">No admin accounts exist yet. Complete the first-time setup.</p>
|
{
|
||||||
<a href="/setup" class="btn btn-primary">Go to Setup</a>
|
<div class="alert alert-danger">@errorMessage</div>
|
||||||
</div>
|
}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" name="username" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" name="password" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="row justify-content-center mt-5">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header"><h4 class="mb-0">Admin Login</h4></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post" action="/admin/login">
|
|
||||||
@if (errorMessage != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">@errorMessage</div>
|
|
||||||
}
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Username</label>
|
|
||||||
<input type="text" name="username" class="form-control" required />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Password</label>
|
|
||||||
<input type="password" name="password" class="form-control" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
@page "/rooms/create"
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
|
|
||||||
<PageTitle>Create Room</PageTitle>
|
|
||||||
|
|
||||||
<h3>Create Room</h3>
|
|
||||||
<p class="text-muted">Create a new room and optionally a default subroom.</p>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Room Details</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Room ID (leave 0 for auto)</label>
|
|
||||||
<input type="number" class="form-control" @bind="roomId" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Name <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" @bind="name" required />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Description</label>
|
|
||||||
<textarea class="form-control" rows="3" @bind="description"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Creator Account ID</label>
|
|
||||||
<input type="number" class="form-control" @bind="creatorAccountId" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Image Name</label>
|
|
||||||
<input type="text" class="form-control" @bind="imageName" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Accessibility</label>
|
|
||||||
<input type="number" class="form-control" @bind="accessibility" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">SubRoom</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="createSubRoom" />
|
|
||||||
<label class="form-check-label">Create a default subroom</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (createSubRoom)
|
|
||||||
{
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">SubRoom Name</label>
|
|
||||||
<input type="text" class="form-control" @bind="subRoomName" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Max Players</label>
|
|
||||||
<input type="number" class="form-control" @bind="maxPlayers" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Platform Support</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="supportsVRLow" />
|
|
||||||
<label class="form-check-label">VR Low</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="supportsQuest2" />
|
|
||||||
<label class="form-check-label">Quest 2</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="supportsMobile" />
|
|
||||||
<label class="form-check-label">Mobile</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="supportsScreens" />
|
|
||||||
<label class="form-check-label">Screens</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="supportsWalkVR" />
|
|
||||||
<label class="form-check-label">Walk VR</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="supportsTeleportVR" />
|
|
||||||
<label class="form-check-label">Teleport VR</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" @bind="supportsJuniors" />
|
|
||||||
<label class="form-check-label">Supports Juniors</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" @onclick="CreateRoom" disabled="@creating">
|
|
||||||
@(creating ? "Creating..." : "Create Room")
|
|
||||||
</button>
|
|
||||||
<a href="rooms/search" class="btn btn-outline-secondary ms-2">Cancel</a>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(message))
|
|
||||||
{
|
|
||||||
<div class="alert mt-3 @(message.Contains("Error") ? "alert-danger" : "alert-success")">@message</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (createdRoom != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success mt-2">
|
|
||||||
Room created! <a href="rooms/edit/@createdRoom.RoomId">Edit Room #@createdRoom.RoomId</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
@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<Room>();
|
|
||||||
message = $"Room '{createdRoom?.Name}' created successfully!";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
message = $"Error: {ex.Message}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
creating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
@page "/setup/accounts"
|
|
||||||
@attribute [Authorize(Policy = "OwnerOnly")]
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
@using CannedNet.Admin.Models
|
|
||||||
@using CannedNet.Admin.Services
|
|
||||||
@inject AdminAuthService AuthService
|
|
||||||
|
|
||||||
<PageTitle>Admin Accounts</PageTitle>
|
|
||||||
|
|
||||||
<h3>Admin Accounts</h3>
|
|
||||||
<p class="text-muted">Manage admin panel accounts. Only the owner can create or delete accounts.</p>
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">Create New Admin Account</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<label class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control" @bind="newUsername" required />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<label class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" @bind="newPassword" required minlength="6" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<label class="form-label">Confirm Password</label>
|
|
||||||
<input type="password" class="form-control" @bind="newConfirmPassword" required />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success" @onclick="CreateAccount" disabled="@creating">
|
|
||||||
@(creating ? "Creating..." : "Create Account")
|
|
||||||
</button>
|
|
||||||
@if (!string.IsNullOrEmpty(createMessage))
|
|
||||||
{
|
|
||||||
<span class="ms-2 @(createMessage.Contains("Error") ? "text-danger" : "text-success")">@createMessage</span>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Admin Accounts</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var user in AuthService.GetAll())
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@user.Id</td>
|
|
||||||
<td>@user.Username</td>
|
|
||||||
<td>
|
|
||||||
@if (user.IsOwner)
|
|
||||||
{
|
|
||||||
<td>Owner</td>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<td>Admin</td>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>@user.CreatedAt.ToString("g")</td>
|
|
||||||
<td>
|
|
||||||
@if (!user.IsOwner)
|
|
||||||
{
|
|
||||||
<button class="btn btn-sm btn-danger" @onclick="() => DeleteUser(user.Id)">Delete</button>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@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<bool>("confirm", "Delete this admin account?");
|
|
||||||
if (!confirmed) return;
|
|
||||||
AuthService.DeleteUser(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
@page "/setup"
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
@using CannedNet.Admin.Services
|
|
||||||
@inject AdminAuthService AuthService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
<PageTitle>First-Time Setup</PageTitle>
|
|
||||||
|
|
||||||
<div class="row justify-content-center mt-5">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header"><h4 class="mb-0">First-Time Setup</h4></div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if (completed)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success">
|
|
||||||
<h5>Owner account created!</h5>
|
|
||||||
<p>You can now <a href="/login">log in</a> with your new admin account.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (AuthService.HasAnyUser())
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
Admin accounts already exist. <a href="/login">Go to login</a>.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p class="text-muted">Create the initial owner account. This account will have full access and can create additional admin accounts.</p>
|
|
||||||
<form>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control" @bind="username" required />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" @bind="password" required minlength="6" />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Confirm Password</label>
|
|
||||||
<input type="password" class="form-control" @bind="confirmPassword" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-100" @onclick="CreateOwner" disabled="@creating">
|
|
||||||
@(creating ? "Creating..." : "Create Owner Account")
|
|
||||||
</button>
|
|
||||||
@if (!string.IsNullOrEmpty(message))
|
|
||||||
{
|
|
||||||
<div class="alert mt-3 @(message.Contains("Error") ? "alert-danger" : "alert-success")">@message</div>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
@page "/setup/nameserver"
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
@using CannedNet.Admin.Models
|
|
||||||
@using System.Text.Json
|
|
||||||
|
|
||||||
<PageTitle>Nameserver Setup</PageTitle>
|
|
||||||
|
|
||||||
<h3>Nameserver Endpoints</h3>
|
|
||||||
<p class="text-muted">Configure the base server URL the game client connects to. All endpoints are derived from this URL.</p>
|
|
||||||
|
|
||||||
@if (loading)
|
|
||||||
{
|
|
||||||
<div class="spinner-border" role="status"></div>
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(error))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">@error</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<form>
|
|
||||||
<div class="mb-3">
|
|
||||||
<button type="submit" class="btn btn-success" @onclick="Save" disabled="@saving">
|
|
||||||
@(saving ? "Saving..." : "Save Changes")
|
|
||||||
</button>
|
|
||||||
@if (!string.IsNullOrEmpty(saveMessage))
|
|
||||||
{
|
|
||||||
<span class="@(saveError ? "text-danger" : "text-success") ms-2">@saveMessage</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="baseUrl">Base URL:</label>
|
|
||||||
<input id="baseUrl" class="form-control font-monospace" @bind="baseUrl" placeholder="https://cannednet.lapis.codes" />
|
|
||||||
<div class="form-text">All endpoint URLs will use this as the base.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Service</th>
|
|
||||||
<th>Full URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var (name, path) in EndpointDefinitions.Endpoints)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td><code>@name</code></td>
|
|
||||||
<td><code>@(baseUrl.TrimEnd('/') + path)</code></td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
+61
-16
@@ -1,8 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using CannedNet.Admin.Components;
|
using CannedNet.Admin.Components;
|
||||||
using CannedNet.Admin.Services;
|
using CannedNet.Admin.Services;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -23,22 +24,17 @@ builder.Services.AddAuthentication("AdminCookie")
|
|||||||
options.SlidingExpiration = true;
|
options.SlidingExpiration = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorization(options =>
|
builder.Services.AddAuthorization();
|
||||||
{
|
|
||||||
options.AddPolicy("OwnerOnly", policy => policy.RequireClaim("role", "Owner"));
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<AdminAuthService>();
|
|
||||||
var imagesBaseUrl = builder.Configuration.GetValue<string>("ImagesBaseUrl") ?? "http://localhost:5000/img";
|
var imagesBaseUrl = builder.Configuration.GetValue<string>("ImagesBaseUrl") ?? "http://localhost:5000/img";
|
||||||
builder.Services.AddSingleton(new ImageUrlService(imagesBaseUrl));
|
builder.Services.AddSingleton(new ImageUrlService(imagesBaseUrl));
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
|
||||||
var serverUrl = builder.Configuration.GetValue<string>("ServerBaseUrl") ?? "http://localhost:5000";
|
var serverUrl = builder.Configuration.GetValue<string>("ServerBaseUrl") ?? "http://localhost:5000";
|
||||||
builder.Services.AddTransient<AdminApiKeyHandler>();
|
builder.Services.AddTransient<JwtBearerHandler>();
|
||||||
builder.Services.AddScoped(sp =>
|
builder.Services.AddScoped(sp =>
|
||||||
{
|
{
|
||||||
var handler = sp.GetRequiredService<AdminApiKeyHandler>();
|
var handler = sp.GetRequiredService<JwtBearerHandler>();
|
||||||
handler.InnerHandler = new SocketsHttpHandler();
|
handler.InnerHandler = new SocketsHttpHandler();
|
||||||
return new HttpClient(handler) { BaseAddress = new Uri(serverUrl) };
|
return new HttpClient(handler) { BaseAddress = new Uri(serverUrl) };
|
||||||
});
|
});
|
||||||
@@ -47,7 +43,7 @@ var app = builder.Build();
|
|||||||
|
|
||||||
// --- Admin Auth Endpoints ---
|
// --- 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 form = await context.Request.ReadFormAsync();
|
||||||
var username = form["username"].FirstOrDefault() ?? "";
|
var username = form["username"].FirstOrDefault() ?? "";
|
||||||
@@ -59,20 +55,55 @@ app.MapPost("/admin/login", async (HttpContext context, AdminAuthService authSer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = authService.GetByUsername(username);
|
var serverUrl = app.Services.GetRequiredService<IConfiguration>().GetValue<string>("ServerBaseUrl") ?? "http://localhost:5000";
|
||||||
if (user == null || !authService.VerifyPassword(password, user.PasswordHash))
|
using var httpClient = new HttpClient { BaseAddress = new Uri(serverUrl) };
|
||||||
|
var tokenResponse = await httpClient.PostAsync("/auth/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["grant_type"] = "password",
|
||||||
|
["username"] = username,
|
||||||
|
["password"] = password
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!tokenResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
context.Response.Redirect("/login?error=invalid");
|
context.Response.Redirect("/login?error=invalid");
|
||||||
return;
|
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()),
|
context.Response.Redirect("/login?error=invalid");
|
||||||
new Claim(ClaimTypes.Name, user.Username),
|
return;
|
||||||
new Claim("role", user.IsOwner ? "Owner" : "Admin")
|
}
|
||||||
|
|
||||||
|
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<Claim>
|
||||||
|
{
|
||||||
|
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 identity = new ClaimsIdentity(claims, "AdminCookie");
|
||||||
var principal = new ClaimsPrincipal(identity);
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
@@ -111,3 +142,17 @@ app.MapRazorComponents<App>()
|
|||||||
.AddInteractiveServerRenderMode();
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
app.Run();
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
namespace CannedNet.Admin.Services;
|
|
||||||
|
|
||||||
public class AdminApiKeyHandler : DelegatingHandler
|
|
||||||
{
|
|
||||||
private readonly string _apiKey;
|
|
||||||
|
|
||||||
public AdminApiKeyHandler(IConfiguration config)
|
|
||||||
{
|
|
||||||
_apiKey = config.GetValue<string>("Admin:ApiKey")
|
|
||||||
?? config.GetValue<string>("AdminApiKey")
|
|
||||||
?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_apiKey))
|
|
||||||
request.Headers.Add("X-Admin-Api-Key", _apiKey);
|
|
||||||
|
|
||||||
return await base.SendAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<AdminUser> _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<AdminUser> 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<List<AdminUser>>(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 { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<HttpResponseMessage> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,5 @@ public class Account
|
|||||||
[JsonPropertyName("username")] public string? Username { get; set; }
|
[JsonPropertyName("username")] public string? Username { get; set; }
|
||||||
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
||||||
[JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
|
[JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
|
||||||
|
[JsonPropertyName("password")] public string? Password { get; set; }
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.2.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.2.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.1" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
|
||||||
|
|||||||
@@ -0,0 +1,1137 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<int>("AccountId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("accountId");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasJsonPropertyName("createdAt");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("displayName");
|
||||||
|
|
||||||
|
b.Property<int?>("IdentityFlags")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("identityFlags");
|
||||||
|
|
||||||
|
b.Property<bool>("IsJunior")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isJunior");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("password");
|
||||||
|
|
||||||
|
b.Property<int?>("PersonalPronouns")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("personalPronouns");
|
||||||
|
|
||||||
|
b.Property<int?>("Platforms")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("platforms");
|
||||||
|
|
||||||
|
b.Property<string>("ProfileImage")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("profileImage");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("username");
|
||||||
|
|
||||||
|
b.HasKey("AccountId");
|
||||||
|
|
||||||
|
b.ToTable("accounts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.AvatarItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AvatarItemDesc")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("avatarItemDesc");
|
||||||
|
|
||||||
|
b.Property<int>("AvatarItemType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("avatarItemType");
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("friendlyName");
|
||||||
|
|
||||||
|
b.Property<int>("OwnerAccountId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("PlatformMask")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("platformMask");
|
||||||
|
|
||||||
|
b.Property<int>("Rarity")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("rarity");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("accountId");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastLoginTime")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasJsonPropertyName("lastLoginTime");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("platform");
|
||||||
|
|
||||||
|
b.Property<string>("PlatformID")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("platformId");
|
||||||
|
|
||||||
|
b.Property<bool>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("imageUrl");
|
||||||
|
|
||||||
|
b.Property<bool>("IsThumbnail")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isThumbnail");
|
||||||
|
|
||||||
|
b.Property<int>("RoomId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("ActiveDurationMinutes")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("ActiveDurationMinutes");
|
||||||
|
|
||||||
|
b.Property<string>("ConsumableItemDesc")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("ConsumableItemDesc");
|
||||||
|
|
||||||
|
b.Property<int>("Count")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("Count");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<List<DateTime>>("CreatedAts")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("timestamp with time zone[]")
|
||||||
|
.HasJsonPropertyName("CreatedAts");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<List<int>>("Ids")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("integer[]")
|
||||||
|
.HasJsonPropertyName("Ids");
|
||||||
|
|
||||||
|
b.Property<int>("InitialCount")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("InitialCount");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("IsActive");
|
||||||
|
|
||||||
|
b.Property<bool>("IsTransferable")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("IsTransferable");
|
||||||
|
|
||||||
|
b.Property<int>("OwnerAccountId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerAccountId");
|
||||||
|
|
||||||
|
b.ToTable("consumable_items", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.Models.EarnableReward", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AvatarItemDesc")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("avatarItemDesc");
|
||||||
|
|
||||||
|
b.Property<int>("AvatarItemType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("avatarItemType");
|
||||||
|
|
||||||
|
b.Property<string>("ConsumableItemDesc")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("consumableItemDesc");
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("friendlyName");
|
||||||
|
|
||||||
|
b.Property<int>("GiftRarity")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("giftRarity");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AvatarItemDesc")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AvatarItemType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConsumableItemDesc")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Context")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Currency")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("CurrencyType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("EquipmentModificationGuid")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("EquipmentPrefabName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("GiftDropId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsQuery")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("ItemSetFriendlyName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("ItemSetId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Level")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Rarity")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("StorefrontItemId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("SubscribersOnly")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Tooltip")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("Unique")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StorefrontItemId");
|
||||||
|
|
||||||
|
b.ToTable("gift_drops", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.Models.ReceivedGift", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AvatarItemDesc")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("avatarItemDesc");
|
||||||
|
|
||||||
|
b.Property<int>("AvatarItemType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("avatarItemType");
|
||||||
|
|
||||||
|
b.Property<int>("BalanceType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("balanceType");
|
||||||
|
|
||||||
|
b.Property<string>("ConsumableItemDesc")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("consumableItemDesc");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ConsumedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasJsonPropertyName("consumedAt");
|
||||||
|
|
||||||
|
b.Property<int>("Currency")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("currency");
|
||||||
|
|
||||||
|
b.Property<int>("CurrencyType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("currencyType");
|
||||||
|
|
||||||
|
b.Property<string>("EquipmentModificationGuid")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("equipmentModificationGuid");
|
||||||
|
|
||||||
|
b.Property<string>("EquipmentPrefabName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("equipmentPrefabName");
|
||||||
|
|
||||||
|
b.Property<string>("FriendlyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("friendlyName");
|
||||||
|
|
||||||
|
b.Property<int?>("FromPlayerId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("fromPlayerId");
|
||||||
|
|
||||||
|
b.Property<int>("GiftContext")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("giftContext");
|
||||||
|
|
||||||
|
b.Property<int>("GiftRarity")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("giftRarity");
|
||||||
|
|
||||||
|
b.Property<bool>("IsConsumed")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isConsumed");
|
||||||
|
|
||||||
|
b.Property<int>("Level")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("level");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("message");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("platform");
|
||||||
|
|
||||||
|
b.Property<int>("PlatformsToSpawnOn")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("platformsToSpawnOn");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasJsonPropertyName("receivedAt");
|
||||||
|
|
||||||
|
b.Property<int>("ReceiverAccountId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("receiverAccountId");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("NextUpdate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("StorefrontType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("storefronts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.Models.StorefrontItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsFeatured")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("NewUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("PurchasableItemId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("StorefrontId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StorefrontId");
|
||||||
|
|
||||||
|
b.ToTable("storefront_items", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.Models.StorefrontPrice", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CurrencyType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Price")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("StorefrontItemId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StorefrontItemId");
|
||||||
|
|
||||||
|
b.ToTable("storefront_prices", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.PlayerAvatar", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("FaceFeatures")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("FaceFeatures");
|
||||||
|
|
||||||
|
b.Property<string>("HairColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("HairColor");
|
||||||
|
|
||||||
|
b.Property<string>("OutfitSelections")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("OutfitSelections");
|
||||||
|
|
||||||
|
b.Property<int>("OwnerAccountId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("accountId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("accountId");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("accountId"));
|
||||||
|
|
||||||
|
b.HasKey("accountId");
|
||||||
|
|
||||||
|
b.ToTable("player_bios", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.PlayerProgression", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Level")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("level");
|
||||||
|
|
||||||
|
b.Property<int>("PlayerId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("playerId");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("key");
|
||||||
|
|
||||||
|
b.Property<int>("PlayerId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("playerId");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("RoomId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Tooltip")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("tooltip");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("type");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("imageUrl");
|
||||||
|
|
||||||
|
b.Property<int>("RoomId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("sortOrder");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Accessibility")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("accessibility");
|
||||||
|
|
||||||
|
b.Property<bool>("CloningAllowed")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("cloningAllowed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasJsonPropertyName("createdAt");
|
||||||
|
|
||||||
|
b.Property<int>("CreatorAccountId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("creatorAccountId");
|
||||||
|
|
||||||
|
b.Property<string>("CustomWarning")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("customWarning");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("description");
|
||||||
|
|
||||||
|
b.Property<bool>("DisableMicAutoMute")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("disableMicAutoMute");
|
||||||
|
|
||||||
|
b.Property<bool>("DisableRoomComments")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("disableRoomComments");
|
||||||
|
|
||||||
|
b.Property<bool>("EncryptVoiceChat")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("encryptVoiceChat");
|
||||||
|
|
||||||
|
b.Property<string>("ImageName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("imageName");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDorm")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isDorm");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRRO")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isRRO");
|
||||||
|
|
||||||
|
b.Property<int>("MinLevel")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("minLevel");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("name");
|
||||||
|
|
||||||
|
b.Property<int>("RoomId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("roomId");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("state");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsJuniors")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsJuniors");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsLevelVoting")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsLevelVoting");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsMobile")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsMobile");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsQuest2")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsQuest2");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsScreens")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsScreens");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsTeleportVR")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsTeleportVR");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsVRLow")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsVRLow");
|
||||||
|
|
||||||
|
b.Property<bool>("SupportsWalkVR")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("supportsWalkVR");
|
||||||
|
|
||||||
|
b.Property<string>("Tags")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("tags");
|
||||||
|
|
||||||
|
b.Property<int>("WarningMask")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("warningMask");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorAccountId");
|
||||||
|
|
||||||
|
b.ToTable("rooms", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.RoomInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("EncryptVoiceChat")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("EncryptVoiceChat");
|
||||||
|
|
||||||
|
b.Property<int>("OwnerAccountId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("clubId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("clubId");
|
||||||
|
|
||||||
|
b.Property<string>("dataBlob")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("dataBlob");
|
||||||
|
|
||||||
|
b.Property<int>("eventId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("eventId");
|
||||||
|
|
||||||
|
b.Property<bool>("isFull")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isFull");
|
||||||
|
|
||||||
|
b.Property<bool>("isInProgress")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isInProgress");
|
||||||
|
|
||||||
|
b.Property<bool>("isPrivate")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isPrivate");
|
||||||
|
|
||||||
|
b.Property<string>("location")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("location");
|
||||||
|
|
||||||
|
b.Property<int>("maxCapacity")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("maxCapacity");
|
||||||
|
|
||||||
|
b.Property<string>("name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("name");
|
||||||
|
|
||||||
|
b.Property<string>("photonRegionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("photonRegionId");
|
||||||
|
|
||||||
|
b.Property<string>("photonRoomId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("photonRoomId");
|
||||||
|
|
||||||
|
b.Property<string>("roomCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("roomCode");
|
||||||
|
|
||||||
|
b.Property<int>("roomId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("roomId");
|
||||||
|
|
||||||
|
b.Property<int>("roomInstanceId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("roomInstanceId");
|
||||||
|
|
||||||
|
b.Property<int>("roomInstanceType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("roomInstanceType");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("accountId");
|
||||||
|
|
||||||
|
b.Property<int>("InvitedRole")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("invitedRole");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("role");
|
||||||
|
|
||||||
|
b.Property<int>("RoomId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoomId");
|
||||||
|
|
||||||
|
b.ToTable("room_roles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CannedNet.SavedOutfit", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("FaceFeatures")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("FaceFeatures");
|
||||||
|
|
||||||
|
b.Property<string>("HairColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("HairColor");
|
||||||
|
|
||||||
|
b.Property<string>("OutfitSelections")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("OutfitSelections");
|
||||||
|
|
||||||
|
b.Property<int>("OwnerAccountId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("PreviewImageName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("PreviewImageName");
|
||||||
|
|
||||||
|
b.Property<string>("SkinColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("SkinColor");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Accessibility")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("accessibility");
|
||||||
|
|
||||||
|
b.Property<string>("DataBlob")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("dataBlob");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DataSavedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasJsonPropertyName("dataSavedAt");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSandbox")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasJsonPropertyName("isSandbox");
|
||||||
|
|
||||||
|
b.Property<int>("MaxPlayers")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("maxPlayers");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("name");
|
||||||
|
|
||||||
|
b.Property<int>("RoomId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SubRoomId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("subRoomId");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Balance")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("Balance");
|
||||||
|
|
||||||
|
b.Property<int>("BalanceType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("BalanceType");
|
||||||
|
|
||||||
|
b.Property<int>("CurrencyType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasJsonPropertyName("CurrencyType");
|
||||||
|
|
||||||
|
b.Property<int>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CannedNet.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAccountPassword : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Password",
|
||||||
|
table: "accounts",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Password",
|
||||||
|
table: "accounts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ namespace CannedNet.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.8")
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -46,6 +46,10 @@ namespace CannedNet.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasJsonPropertyName("isJunior");
|
.HasJsonPropertyName("isJunior");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasJsonPropertyName("password");
|
||||||
|
|
||||||
b.Property<int?>("PersonalPronouns")
|
b.Property<int?>("PersonalPronouns")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasJsonPropertyName("personalPronouns");
|
.HasJsonPropertyName("personalPronouns");
|
||||||
|
|||||||
+10
-54
@@ -39,6 +39,7 @@ public static class Program
|
|||||||
builder.Services.AddSingleton<NotificationService>();
|
builder.Services.AddSingleton<NotificationService>();
|
||||||
builder.Services.AddScoped<StorefrontFillService>();
|
builder.Services.AddScoped<StorefrontFillService>();
|
||||||
builder.Services.AddScoped<JwtTokenService>();
|
builder.Services.AddScoped<JwtTokenService>();
|
||||||
|
builder.Services.AddSingleton<ConfigService>();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
builder.Services.AddAuthentication(options =>
|
builder.Services.AddAuthentication(options =>
|
||||||
@@ -70,7 +71,7 @@ public static class Program
|
|||||||
ClockSkew = TimeSpan.Zero,
|
ClockSkew = TimeSpan.Zero,
|
||||||
|
|
||||||
NameClaimType = ClaimTypes.NameIdentifier,
|
NameClaimType = ClaimTypes.NameIdentifier,
|
||||||
RoleClaimType = "role"
|
RoleClaimType = ClaimTypes.Role
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.AddScheme<AuthenticationSchemeOptions, AdminApiKeyHandler>("AdminApiKey", null);
|
.AddScheme<AuthenticationSchemeOptions, AdminApiKeyHandler>("AdminApiKey", null);
|
||||||
@@ -89,14 +90,15 @@ public static class Program
|
|||||||
IHubContext<NotificationsHub> hubContext = app.Services.GetRequiredService<IHubContext<NotificationsHub>>();
|
IHubContext<NotificationsHub> hubContext = app.Services.GetRequiredService<IHubContext<NotificationsHub>>();
|
||||||
NotificationService.SetHubContext(hubContext);
|
NotificationService.SetHubContext(hubContext);
|
||||||
|
|
||||||
//if (app.Environment.IsDevelopment()) {
|
if (app.Environment.IsDevelopment())
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI(options =>
|
|
||||||
{
|
{
|
||||||
options.SwaggerEndpoint("/swagger/v1/swagger.json", "CannedNet v1");
|
app.UseSwagger();
|
||||||
options.RoutePrefix = "swagger/ui/";
|
app.UseSwaggerUI(options =>
|
||||||
});
|
{
|
||||||
//}
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "CannedNet v1");
|
||||||
|
options.RoutePrefix = "swagger/ui/";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
using IServiceScope scope = app.Services.CreateScope();
|
using IServiceScope scope = app.Services.CreateScope();
|
||||||
AppDbContext db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
AppDbContext db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
@@ -117,50 +119,4 @@ public static class Program
|
|||||||
|
|
||||||
await app.RunAsync();
|
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<CannedNet.Data.AppDbContext>();
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// dbContext.Database.Migrate();
|
|
||||||
// }
|
|
||||||
// catch (Exception ex)
|
|
||||||
// {
|
|
||||||
// dbContext.Database.EnsureCreated();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // automatically fill out storefronts tables from the JSON's
|
|
||||||
// var seedingService = scope.ServiceProvider.GetRequiredService<StorefrontFillService>();
|
|
||||||
// await seedingService.FillStorefrontsAsync();
|
|
||||||
//
|
|
||||||
// var jwtService = scope.ServiceProvider.GetRequiredService<JwtTokenService>();
|
|
||||||
//
|
|
||||||
// foreach (var (app, service) in apps)
|
|
||||||
// {
|
|
||||||
// service.MapEndpoints(app, jwtService);
|
|
||||||
// app.UseRequestLogging();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await Task.WhenAll(apps.Select(t => t.App.RunAsync()));
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<AppConfig>(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<Dictionary<string, string>>(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<string, string>(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<string, string> 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<string>(),
|
||||||
|
VModUserIds = new List<string>(),
|
||||||
|
AutoFillRROs = true,
|
||||||
|
AutoFillStorefronts = true,
|
||||||
|
WhitelistOn = false,
|
||||||
|
WhitelistedPlatformIds = new List<string>(),
|
||||||
|
EnableAccountCreation = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppConfig
|
||||||
|
{
|
||||||
|
public string BaseUrl { get; set; } = "";
|
||||||
|
public bool AutoFillRROs { get; set; } = true;
|
||||||
|
public bool AutoFillStorefronts { get; set; } = true;
|
||||||
|
public List<string> DeveloperUserIds { get; set; } = new();
|
||||||
|
public List<string> VModUserIds { get; set; } = new();
|
||||||
|
public List<string> AdminAccountIds { get; set; } = new();
|
||||||
|
public bool WhitelistOn { get; set; } = false;
|
||||||
|
public List<string> WhitelistedPlatformIds { get; set; } = new();
|
||||||
|
public bool EnableAccountCreation { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using CannedNet.Data;
|
using CannedNet.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
@@ -100,132 +101,6 @@ public class AccountsController : ControllerBase
|
|||||||
return Results.Json(result);
|
return Results.Json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("account/create")]
|
|
||||||
public async Task<IResult> 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")]
|
[HttpPut("me/displayname")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IResult> PutMeDisplayName(AppDbContext db)
|
public async Task<IResult> PutMeDisplayName(AppDbContext db)
|
||||||
@@ -320,6 +195,54 @@ public class AccountsController : ControllerBase
|
|||||||
return Results.Ok(RecNetResult.Ok());
|
return Results.Ok(RecNetResult.Ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("me/changepassword")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IResult> 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>();
|
||||||
|
account.Password = hasher.HashPassword(account, newPassword);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Ok(RecNetResult.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/bio")]
|
[HttpGet("{id}/bio")]
|
||||||
public async Task<IResult> GetBio(string id, AppDbContext db)
|
public async Task<IResult> GetBio(string id, AppDbContext db)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,27 +8,11 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
namespace CannedNet.Services.Controllers;
|
namespace CannedNet.Services.Controllers;
|
||||||
|
|
||||||
[ApiController, Route("admin/api")]
|
[ApiController, Route("admin/api")]
|
||||||
[Authorize(AuthenticationSchemes = "AdminApiKey")]
|
[Authorize(Roles = "developer,admin")]
|
||||||
public class AdminController : ControllerBase
|
public class AdminController : ControllerBase
|
||||||
{
|
{
|
||||||
private const string JsonDir = "JSON";
|
private const string JsonDir = "JSON";
|
||||||
|
|
||||||
[HttpGet("endpoints")]
|
|
||||||
public async Task<IResult> GetEndpoints()
|
|
||||||
{
|
|
||||||
var json = await System.IO.File.ReadAllTextAsync(Path.Combine(JsonDir, "endpoints.json"));
|
|
||||||
return Results.Content(json, "application/json");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("endpoints")]
|
|
||||||
public async Task<IResult> 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")]
|
[HttpGet("accounts/search")]
|
||||||
public async Task<IResult> SearchAccounts([FromQuery] string q, AppDbContext db)
|
public async Task<IResult> SearchAccounts([FromQuery] string q, AppDbContext db)
|
||||||
{
|
{
|
||||||
@@ -215,98 +199,6 @@ public class AdminController : ControllerBase
|
|||||||
return Results.Ok(new { success = true });
|
return Results.Ok(new { success = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("rooms")]
|
|
||||||
public async Task<IResult> CreateRoom(AppDbContext db)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(Request.Body);
|
|
||||||
var body = await reader.ReadToEndAsync();
|
|
||||||
var data = JsonSerializer.Deserialize<JsonElement>(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<IResult> 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<JsonElement>(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")]
|
[HttpGet("config/gameconfigs")]
|
||||||
public async Task<IResult> GetGameConfigs()
|
public async Task<IResult> GetGameConfigs()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CannedNet.Data;
|
using CannedNet.Data;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@@ -26,13 +27,16 @@ public class AuthController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("connect/token")]
|
[HttpPost("connect/token")]
|
||||||
public async Task<IResult> ConnectToken(AppDbContext db, JwtTokenService jwtService)
|
public async Task<IResult> ConnectToken(AppDbContext db, JwtTokenService jwtService, ConfigService config)
|
||||||
{
|
{
|
||||||
string grantType = "";
|
string grantType = "";
|
||||||
string accountId = "";
|
string accountId = "";
|
||||||
string platformId = "";
|
string platformId = "";
|
||||||
string platform = "";
|
string platform = "";
|
||||||
int platformInt = 0;
|
int platformInt = 0;
|
||||||
|
string password = "";
|
||||||
|
string username = "";
|
||||||
|
List<string> adminRoles = [];
|
||||||
|
|
||||||
if (HttpContext.Request.ContentLength is > 0)
|
if (HttpContext.Request.ContentLength is > 0)
|
||||||
{
|
{
|
||||||
@@ -61,11 +65,22 @@ public class AuthController : ControllerBase
|
|||||||
break;
|
break;
|
||||||
case "platform_id":
|
case "platform_id":
|
||||||
platformId = value;
|
platformId = value;
|
||||||
|
if (config.Config.WhitelistOn)
|
||||||
|
{
|
||||||
|
if (!config.Config.WhitelistedPlatformIds.Contains(platformId))
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "platform" when int.TryParse(value, out var p):
|
case "platform" when int.TryParse(value, out var p):
|
||||||
platformInt = p;
|
platformInt = p;
|
||||||
platform = ((PlatformType)p).ToString();
|
platform = ((PlatformType)p).ToString();
|
||||||
break;
|
break;
|
||||||
|
case "username":
|
||||||
|
username = value;
|
||||||
|
break;
|
||||||
|
case "password":
|
||||||
|
password = value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +91,11 @@ public class AuthController : ControllerBase
|
|||||||
|
|
||||||
if (grantType == "create_account")
|
if (grantType == "create_account")
|
||||||
{
|
{
|
||||||
|
if (!config.Config.EnableAccountCreation)
|
||||||
|
{
|
||||||
|
return Results.Ok(RecNetResult.Err("Account creation is disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
int newId = new Random().Next(10000, 99999);
|
int newId = new Random().Next(10000, 99999);
|
||||||
Account account = new()
|
Account account = new()
|
||||||
{
|
{
|
||||||
@@ -159,8 +179,27 @@ public class AuthController : ControllerBase
|
|||||||
|
|
||||||
accountId = newId.ToString();
|
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<Account>();
|
||||||
|
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))
|
if (!string.IsNullOrEmpty(accountId) && int.TryParse(accountId, out var id))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -421,9 +421,9 @@ public class MatchmakingController : ControllerBase
|
|||||||
|
|
||||||
return Results.Json(new
|
return Results.Json(new
|
||||||
{
|
{
|
||||||
playerId = heartbeat.playerId != 0 ? heartbeat.playerId : id,
|
PlayerId = heartbeat.playerId != 0 ? heartbeat.playerId : id,
|
||||||
statusVisibility = heartbeat.statusVisibility,
|
StatusVisibility = heartbeat.statusVisibility,
|
||||||
deviceClass = heartbeat.deviceClass,
|
DeviceClass = heartbeat.deviceClass,
|
||||||
vrMovementMode = heartbeat.vrMovementMode != 0 ? heartbeat.vrMovementMode : 1,
|
vrMovementMode = heartbeat.vrMovementMode != 0 ? heartbeat.vrMovementMode : 1,
|
||||||
roomInstance = roomInstance != null ? new
|
roomInstance = roomInstance != null ? new
|
||||||
{
|
{
|
||||||
@@ -445,9 +445,9 @@ public class MatchmakingController : ControllerBase
|
|||||||
isInProgress = roomInstance.isInProgress,
|
isInProgress = roomInstance.isInProgress,
|
||||||
EncryptVoiceChat = roomInstance.EncryptVoiceChat
|
EncryptVoiceChat = roomInstance.EncryptVoiceChat
|
||||||
} : null,
|
} : null,
|
||||||
isOnline = roomInstance != null,
|
IsOnline = true,
|
||||||
appVersion = heartbeat.appVersion ?? "",
|
AppVersion = heartbeat.appVersion ?? "",
|
||||||
platform = heartbeat.platform
|
Platform = heartbeat.platform
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace CannedNet.Services.Controllers;
|
namespace CannedNet.Services.Controllers;
|
||||||
|
|
||||||
//TODO: proper NameServer configs
|
|
||||||
[ApiController, Route("ns")]
|
[ApiController, Route("ns")]
|
||||||
public class NSController : ControllerBase
|
public class NSController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class JwtTokenService
|
|||||||
_signingCredentials = new SigningCredentials(_securityKey, SecurityAlgorithms.RsaSha256);
|
_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<string>? additionalRoles = null, string amr = "cached_login")
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var exp = now.AddHours(1);
|
var exp = now.AddHours(1);
|
||||||
@@ -38,7 +38,7 @@ public class JwtTokenService
|
|||||||
new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
|
new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
|
||||||
new Claim("exp", ((DateTimeOffset)exp).ToUnixTimeSeconds().ToString()),
|
new Claim("exp", ((DateTimeOffset)exp).ToUnixTimeSeconds().ToString()),
|
||||||
new Claim("aud", "https://lapis.codes/resources"),
|
new Claim("aud", "https://lapis.codes/resources"),
|
||||||
new Claim("amr", "cached_login"),
|
new Claim("amr", amr),
|
||||||
new Claim("client_id", "recroom"),
|
new Claim("client_id", "recroom"),
|
||||||
new Claim("sub", accountId),
|
new Claim("sub", accountId),
|
||||||
new Claim("auth_time", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
|
new Claim("auth_time", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
|
||||||
@@ -48,11 +48,25 @@ public class JwtTokenService
|
|||||||
new Claim("rn.ver", "20210129"),
|
new Claim("rn.ver", "20210129"),
|
||||||
new Claim("rn.plat", "0"),
|
new Claim("rn.plat", "0"),
|
||||||
new Claim("role", "gameClient"),
|
new Claim("role", "gameClient"),
|
||||||
new Claim("role", "developer"),
|
new Claim("jti", Guid.NewGuid().ToString("N")[..32].ToUpper())
|
||||||
new Claim("role", "moderator"),
|
|
||||||
new Claim("jti", Guid.NewGuid().ToString("N").Substring(0, 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", "profile"));
|
||||||
claims.Add(new Claim("scope", "rn"));
|
claims.Add(new Claim("scope", "rn"));
|
||||||
claims.Add(new Claim("scope", "rn.accounts"));
|
claims.Add(new Claim("scope", "rn.accounts"));
|
||||||
@@ -80,6 +94,8 @@ public class JwtTokenService
|
|||||||
expires: exp,
|
expires: exp,
|
||||||
signingCredentials: _signingCredentials);
|
signingCredentials: _signingCredentials);
|
||||||
|
|
||||||
|
token.Payload["amr"] = new List<string> { amr };
|
||||||
|
|
||||||
return handler.WriteToken(token);
|
return handler.WriteToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ public class StorefrontFillService
|
|||||||
private readonly AppDbContext _dbContext;
|
private readonly AppDbContext _dbContext;
|
||||||
private readonly ILogger<StorefrontFillService> _logger;
|
private readonly ILogger<StorefrontFillService> _logger;
|
||||||
private readonly IWebHostEnvironment _environment;
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
private readonly ConfigService _config;
|
||||||
|
|
||||||
public StorefrontFillService(AppDbContext dbContext, ILogger<StorefrontFillService> logger, IWebHostEnvironment environment)
|
public StorefrontFillService(AppDbContext dbContext, ILogger<StorefrontFillService> logger, IWebHostEnvironment environment, ConfigService config)
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task FillStorefrontsAsync()
|
public async Task FillStorefrontsAsync()
|
||||||
@@ -27,6 +29,11 @@ public class StorefrontFillService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_config.Config.AutoFillStorefronts)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("automatically filling out storefronts due to no data");
|
_logger.LogInformation("automatically filling out storefronts due to no data");
|
||||||
|
|
||||||
var storefronts = new[]
|
var storefronts = new[]
|
||||||
|
|||||||
@@ -11,9 +11,5 @@
|
|||||||
"Ports": {
|
"Ports": {
|
||||||
"Primary": 5000
|
"Primary": 5000
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*"
|
||||||
"Admin": {
|
|
||||||
"ApiKey": "changeme-replace-with-a-secure-key"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user