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/Data/PublicKey.pem
|
||||
CannedNet.Admin/admin_users.json
|
||||
CannedNet/config.json
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
@using CannedNet.Admin.Services
|
||||
@inherits LayoutComponentBase
|
||||
@inject AdminAuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@{
|
||||
var body = Body;
|
||||
|
||||
@@ -16,18 +16,6 @@
|
||||
</NavLink>
|
||||
</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-item px-3">
|
||||
<NavLink class="nav-link" href="users/search">
|
||||
@@ -41,18 +29,8 @@
|
||||
<span class="bi bi-building-nav-menu" aria-hidden="true"></span> Search Rooms
|
||||
</NavLink>
|
||||
</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-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">
|
||||
<NavLink class="nav-link" href="config/gameconfig">
|
||||
<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
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (!AuthService.HasAnyUser())
|
||||
Navigation.NavigateTo("/setup", forceLoad: true);
|
||||
else
|
||||
Navigation.NavigateTo("/login", forceLoad: true);
|
||||
Navigation.NavigateTo("/login", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
@page "/change-password"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
@using System.Security.Claims
|
||||
@using CannedNet.Admin.Services
|
||||
@inject AdminAuthService AuthService
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
@@ -44,8 +42,6 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private string currentPassword = "";
|
||||
private string newPassword = "";
|
||||
private string confirmNewPassword = "";
|
||||
@@ -75,17 +71,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var state = await AuthState!;
|
||||
var userIdClaim = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userIdClaim == null || !int.TryParse(userIdClaim, out var userId))
|
||||
var content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
message = "Error: Could not identify user.";
|
||||
return;
|
||||
}
|
||||
["newPassword"] = newPassword
|
||||
});
|
||||
|
||||
if (!AuthService.ChangePassword(userId, currentPassword, newPassword))
|
||||
var response = await Http.PutAsync("/accounts/me/changepassword", content);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
message = "Error: Current password is incorrect.";
|
||||
message = "Error: Failed to change password.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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="card">
|
||||
<div class="card-body">
|
||||
|
||||
@@ -1,52 +1,33 @@
|
||||
@page "/login"
|
||||
@rendermode InteractiveServer
|
||||
@using CannedNet.Admin.Services
|
||||
@inject AdminAuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>Login</PageTitle>
|
||||
|
||||
@if (AuthService.HasAnyUser() == false)
|
||||
{
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h4>No Admin Accounts</h4>
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
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>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
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.Text;
|
||||
using System.Text.Json;
|
||||
using CannedNet.Admin.Components;
|
||||
using CannedNet.Admin.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -23,22 +24,17 @@ builder.Services.AddAuthentication("AdminCookie")
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("OwnerOnly", policy => policy.RequireClaim("role", "Owner"));
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddSingleton<AdminAuthService>();
|
||||
var imagesBaseUrl = builder.Configuration.GetValue<string>("ImagesBaseUrl") ?? "http://localhost:5000/img";
|
||||
builder.Services.AddSingleton(new ImageUrlService(imagesBaseUrl));
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
|
||||
var serverUrl = builder.Configuration.GetValue<string>("ServerBaseUrl") ?? "http://localhost:5000";
|
||||
builder.Services.AddTransient<AdminApiKeyHandler>();
|
||||
builder.Services.AddTransient<JwtBearerHandler>();
|
||||
builder.Services.AddScoped(sp =>
|
||||
{
|
||||
var handler = sp.GetRequiredService<AdminApiKeyHandler>();
|
||||
var handler = sp.GetRequiredService<JwtBearerHandler>();
|
||||
handler.InnerHandler = new SocketsHttpHandler();
|
||||
return new HttpClient(handler) { BaseAddress = new Uri(serverUrl) };
|
||||
});
|
||||
@@ -47,7 +43,7 @@ var app = builder.Build();
|
||||
|
||||
// --- Admin Auth Endpoints ---
|
||||
|
||||
app.MapPost("/admin/login", async (HttpContext context, AdminAuthService authService) =>
|
||||
app.MapPost("/admin/login", async (HttpContext context) =>
|
||||
{
|
||||
var form = await context.Request.ReadFormAsync();
|
||||
var username = form["username"].FirstOrDefault() ?? "";
|
||||
@@ -59,20 +55,55 @@ app.MapPost("/admin/login", async (HttpContext context, AdminAuthService authSer
|
||||
return;
|
||||
}
|
||||
|
||||
var user = authService.GetByUsername(username);
|
||||
if (user == null || !authService.VerifyPassword(password, user.PasswordHash))
|
||||
var serverUrl = app.Services.GetRequiredService<IConfiguration>().GetValue<string>("ServerBaseUrl") ?? "http://localhost:5000";
|
||||
using var httpClient = new HttpClient { BaseAddress = new Uri(serverUrl) };
|
||||
var tokenResponse = await httpClient.PostAsync("/auth/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "password",
|
||||
["username"] = username,
|
||||
["password"] = password
|
||||
}));
|
||||
|
||||
if (!tokenResponse.IsSuccessStatusCode)
|
||||
{
|
||||
context.Response.Redirect("/login?error=invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new[]
|
||||
var json = await tokenResponse.Content.ReadAsStringAsync();
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
var accessToken = doc.RootElement.GetProperty("access_token").GetString();
|
||||
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, user.Username),
|
||||
new Claim("role", user.IsOwner ? "Owner" : "Admin")
|
||||
context.Response.Redirect("/login?error=invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
var payloadJson = DecodeJwtPayload(accessToken);
|
||||
using var payload = JsonDocument.Parse(payloadJson);
|
||||
var accountId = payload.RootElement.TryGetProperty("sub", out var sub) ? sub.GetString() ?? "" : "";
|
||||
var role = "";
|
||||
if (payload.RootElement.TryGetProperty("role", out var r))
|
||||
{
|
||||
if (r.ValueKind == JsonValueKind.String)
|
||||
role = r.GetString() ?? "";
|
||||
else if (r.ValueKind == JsonValueKind.Array)
|
||||
role = string.Join(",", r.EnumerateArray().Select(x => x.GetString() ?? ""));
|
||||
}
|
||||
var name = payload.RootElement.TryGetProperty("platform_id", out var pid) ? pid.GetString() ?? username : username;
|
||||
|
||||
var claims = new List<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 principal = new ClaimsPrincipal(identity);
|
||||
|
||||
@@ -111,3 +142,17 @@ app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
|
||||
static string DecodeJwtPayload(string jwt)
|
||||
{
|
||||
var parts = jwt.Split('.');
|
||||
if (parts.Length < 2) return "{}";
|
||||
var payload = parts[1];
|
||||
switch (payload.Length % 4)
|
||||
{
|
||||
case 2: payload += "=="; break;
|
||||
case 3: payload += "="; break;
|
||||
}
|
||||
var bytes = Convert.FromBase64String(payload);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
@@ -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("displayName")] public string? DisplayName { 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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</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.SwaggerUI" Version="10.2.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
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -46,6 +46,10 @@ namespace CannedNet.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasJsonPropertyName("isJunior");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasJsonPropertyName("password");
|
||||
|
||||
b.Property<int?>("PersonalPronouns")
|
||||
.HasColumnType("integer")
|
||||
.HasJsonPropertyName("personalPronouns");
|
||||
|
||||
+10
-54
@@ -39,6 +39,7 @@ public static class Program
|
||||
builder.Services.AddSingleton<NotificationService>();
|
||||
builder.Services.AddScoped<StorefrontFillService>();
|
||||
builder.Services.AddScoped<JwtTokenService>();
|
||||
builder.Services.AddSingleton<ConfigService>();
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
@@ -70,7 +71,7 @@ public static class Program
|
||||
ClockSkew = TimeSpan.Zero,
|
||||
|
||||
NameClaimType = ClaimTypes.NameIdentifier,
|
||||
RoleClaimType = "role"
|
||||
RoleClaimType = ClaimTypes.Role
|
||||
};
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, AdminApiKeyHandler>("AdminApiKey", null);
|
||||
@@ -89,14 +90,15 @@ public static class Program
|
||||
IHubContext<NotificationsHub> hubContext = app.Services.GetRequiredService<IHubContext<NotificationsHub>>();
|
||||
NotificationService.SetHubContext(hubContext);
|
||||
|
||||
//if (app.Environment.IsDevelopment()) {
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
options.SwaggerEndpoint("/swagger/v1/swagger.json", "CannedNet v1");
|
||||
options.RoutePrefix = "swagger/ui/";
|
||||
});
|
||||
//}
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.SwaggerEndpoint("/swagger/v1/swagger.json", "CannedNet v1");
|
||||
options.RoutePrefix = "swagger/ui/";
|
||||
});
|
||||
}
|
||||
|
||||
using IServiceScope scope = app.Services.CreateScope();
|
||||
AppDbContext db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
@@ -117,50 +119,4 @@ public static class Program
|
||||
|
||||
await app.RunAsync();
|
||||
}
|
||||
|
||||
//public static async Task T() {
|
||||
// List<(WebApplication App, ServiceRegistry Service)> apps = new();
|
||||
//
|
||||
// foreach (var service in Services.Infrastructure.Services.All)
|
||||
// {
|
||||
// var builder = WebApplication.CreateBuilder();
|
||||
//
|
||||
// builder.Services.AddLogging(logging =>
|
||||
// {
|
||||
// logging.AddConsole();
|
||||
// logging.SetMinimumLevel(LogLevel.Debug);
|
||||
// });
|
||||
//
|
||||
// service.ConfigureBuilder(builder);
|
||||
// builder.WebHost.UseUrls($"http://*:{service.Port}");
|
||||
// builder.Configuration.AddJsonFile($"AppConfigs/appsettings.{service.Name}.json", optional: true, reloadOnChange: true);
|
||||
//
|
||||
// apps.Add((builder.Build(), service));
|
||||
// }
|
||||
//
|
||||
// using var scope = apps[0].App.Services.CreateScope();
|
||||
// var dbContext = scope.ServiceProvider.GetRequiredService<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 Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
@@ -100,132 +101,6 @@ public class AccountsController : ControllerBase
|
||||
return Results.Json(result);
|
||||
}
|
||||
|
||||
[HttpPost("account/create")]
|
||||
public async Task<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")]
|
||||
[Authorize]
|
||||
public async Task<IResult> PutMeDisplayName(AppDbContext db)
|
||||
@@ -320,6 +195,54 @@ public class AccountsController : ControllerBase
|
||||
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")]
|
||||
public async Task<IResult> GetBio(string id, AppDbContext db)
|
||||
{
|
||||
|
||||
@@ -8,27 +8,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace CannedNet.Services.Controllers;
|
||||
|
||||
[ApiController, Route("admin/api")]
|
||||
[Authorize(AuthenticationSchemes = "AdminApiKey")]
|
||||
[Authorize(Roles = "developer,admin")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private const string JsonDir = "JSON";
|
||||
|
||||
[HttpGet("endpoints")]
|
||||
public async Task<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")]
|
||||
public async Task<IResult> SearchAccounts([FromQuery] string q, AppDbContext db)
|
||||
{
|
||||
@@ -215,98 +199,6 @@ public class AdminController : ControllerBase
|
||||
return Results.Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPost("rooms")]
|
||||
public async Task<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")]
|
||||
public async Task<IResult> GetGameConfigs()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CannedNet.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -26,13 +27,16 @@ public class AuthController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPost("connect/token")]
|
||||
public async Task<IResult> ConnectToken(AppDbContext db, JwtTokenService jwtService)
|
||||
public async Task<IResult> ConnectToken(AppDbContext db, JwtTokenService jwtService, ConfigService config)
|
||||
{
|
||||
string grantType = "";
|
||||
string accountId = "";
|
||||
string platformId = "";
|
||||
string platform = "";
|
||||
int platformInt = 0;
|
||||
string password = "";
|
||||
string username = "";
|
||||
List<string> adminRoles = [];
|
||||
|
||||
if (HttpContext.Request.ContentLength is > 0)
|
||||
{
|
||||
@@ -61,11 +65,22 @@ public class AuthController : ControllerBase
|
||||
break;
|
||||
case "platform_id":
|
||||
platformId = value;
|
||||
if (config.Config.WhitelistOn)
|
||||
{
|
||||
if (!config.Config.WhitelistedPlatformIds.Contains(platformId))
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
break;
|
||||
case "platform" when int.TryParse(value, out var p):
|
||||
platformInt = p;
|
||||
platform = ((PlatformType)p).ToString();
|
||||
break;
|
||||
case "username":
|
||||
username = value;
|
||||
break;
|
||||
case "password":
|
||||
password = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +91,11 @@ public class AuthController : ControllerBase
|
||||
|
||||
if (grantType == "create_account")
|
||||
{
|
||||
if (!config.Config.EnableAccountCreation)
|
||||
{
|
||||
return Results.Ok(RecNetResult.Err("Account creation is disabled"));
|
||||
}
|
||||
|
||||
int newId = new Random().Next(10000, 99999);
|
||||
Account account = new()
|
||||
{
|
||||
@@ -159,8 +179,27 @@ public class AuthController : ControllerBase
|
||||
|
||||
accountId = newId.ToString();
|
||||
}
|
||||
else if (grantType == "password")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return Results.Ok(RecNetResult.Err("Username and password are required"));
|
||||
|
||||
string accessToken = jwtService.GenerateToken(accountId, platformId, platform);
|
||||
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Username == username);
|
||||
if (account == null || string.IsNullOrEmpty(account.Password))
|
||||
return Results.Ok(RecNetResult.Err("Invalid username or password"));
|
||||
|
||||
var hasher = new PasswordHasher<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))
|
||||
{
|
||||
|
||||
@@ -421,9 +421,9 @@ public class MatchmakingController : ControllerBase
|
||||
|
||||
return Results.Json(new
|
||||
{
|
||||
playerId = heartbeat.playerId != 0 ? heartbeat.playerId : id,
|
||||
statusVisibility = heartbeat.statusVisibility,
|
||||
deviceClass = heartbeat.deviceClass,
|
||||
PlayerId = heartbeat.playerId != 0 ? heartbeat.playerId : id,
|
||||
StatusVisibility = heartbeat.statusVisibility,
|
||||
DeviceClass = heartbeat.deviceClass,
|
||||
vrMovementMode = heartbeat.vrMovementMode != 0 ? heartbeat.vrMovementMode : 1,
|
||||
roomInstance = roomInstance != null ? new
|
||||
{
|
||||
@@ -445,9 +445,9 @@ public class MatchmakingController : ControllerBase
|
||||
isInProgress = roomInstance.isInProgress,
|
||||
EncryptVoiceChat = roomInstance.EncryptVoiceChat
|
||||
} : null,
|
||||
isOnline = roomInstance != null,
|
||||
appVersion = heartbeat.appVersion ?? "",
|
||||
platform = heartbeat.platform
|
||||
IsOnline = true,
|
||||
AppVersion = heartbeat.appVersion ?? "",
|
||||
Platform = heartbeat.platform
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CannedNet.Services.Controllers;
|
||||
|
||||
//TODO: proper NameServer configs
|
||||
[ApiController, Route("ns")]
|
||||
public class NSController : ControllerBase
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public class JwtTokenService
|
||||
_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 exp = now.AddHours(1);
|
||||
@@ -38,7 +38,7 @@ public class JwtTokenService
|
||||
new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
|
||||
new Claim("exp", ((DateTimeOffset)exp).ToUnixTimeSeconds().ToString()),
|
||||
new Claim("aud", "https://lapis.codes/resources"),
|
||||
new Claim("amr", "cached_login"),
|
||||
new Claim("amr", amr),
|
||||
new Claim("client_id", "recroom"),
|
||||
new Claim("sub", accountId),
|
||||
new Claim("auth_time", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
|
||||
@@ -48,11 +48,25 @@ public class JwtTokenService
|
||||
new Claim("rn.ver", "20210129"),
|
||||
new Claim("rn.plat", "0"),
|
||||
new Claim("role", "gameClient"),
|
||||
new Claim("role", "developer"),
|
||||
new Claim("role", "moderator"),
|
||||
new Claim("jti", Guid.NewGuid().ToString("N").Substring(0, 32).ToUpper())
|
||||
new Claim("jti", Guid.NewGuid().ToString("N")[..32].ToUpper())
|
||||
};
|
||||
|
||||
if (config.Config.DeveloperUserIds.Contains(platformId))
|
||||
{
|
||||
claims.Add(new Claim("role", "developer"));
|
||||
}
|
||||
|
||||
if (config.Config.VModUserIds.Contains(platformId))
|
||||
{
|
||||
claims.Add(new Claim("role", "moderator"));
|
||||
}
|
||||
|
||||
if (additionalRoles != null)
|
||||
{
|
||||
foreach (var role in additionalRoles)
|
||||
claims.Add(new Claim("role", role));
|
||||
}
|
||||
|
||||
claims.Add(new Claim("scope", "profile"));
|
||||
claims.Add(new Claim("scope", "rn"));
|
||||
claims.Add(new Claim("scope", "rn.accounts"));
|
||||
@@ -80,6 +94,8 @@ public class JwtTokenService
|
||||
expires: exp,
|
||||
signingCredentials: _signingCredentials);
|
||||
|
||||
token.Payload["amr"] = new List<string> { amr };
|
||||
|
||||
return handler.WriteToken(token);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ public class StorefrontFillService
|
||||
private readonly AppDbContext _dbContext;
|
||||
private readonly ILogger<StorefrontFillService> _logger;
|
||||
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;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task FillStorefrontsAsync()
|
||||
@@ -27,6 +29,11 @@ public class StorefrontFillService
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_config.Config.AutoFillStorefronts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("automatically filling out storefronts due to no data");
|
||||
|
||||
var storefronts = new[]
|
||||
|
||||
@@ -11,9 +11,5 @@
|
||||
"Ports": {
|
||||
"Primary": 5000
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Admin": {
|
||||
"ApiKey": "changeme-replace-with-a-secure-key"
|
||||
}
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user