Initial commit: RR3 Community Server with web admin panel
- ASP.NET Core 8 REST API server - 12 API endpoints matching EA Synergy protocol - SQLite database with Entity Framework Core - Web admin panel with Bootstrap 5 - User, Catalog, Session, Purchase management - Comprehensive documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
225
RR3CommunityServer/Pages/Admin.cshtml
Normal file
225
RR3CommunityServer/Pages/Admin.cshtml
Normal file
@@ -0,0 +1,225 @@
|
||||
@page "/admin"
|
||||
@model RR3CommunityServer.Pages.AdminModel
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
ViewData["Title"] = "Dashboard";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="display-4">🏎️ RR3 Community Server</h1>
|
||||
<p class="text-muted">Administration Dashboard</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="badge bg-success fs-6">🟢 Server Online</div>
|
||||
<div class="text-muted small mt-1">Uptime: @Model.Uptime</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-muted mb-0">Total Users</h6>
|
||||
<h2 class="mb-0">@Model.TotalUsers</h2>
|
||||
</div>
|
||||
<div class="fs-1 text-primary">👥</div>
|
||||
</div>
|
||||
<small class="text-muted">Registered players</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-muted mb-0">Active Sessions</h6>
|
||||
<h2 class="mb-0">@Model.ActiveSessions</h2>
|
||||
</div>
|
||||
<div class="fs-1 text-success">🔄</div>
|
||||
</div>
|
||||
<small class="text-muted">Currently online</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card border-info">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-muted mb-0">Total Devices</h6>
|
||||
<h2 class="mb-0">@Model.TotalDevices</h2>
|
||||
</div>
|
||||
<div class="fs-1 text-info">📱</div>
|
||||
</div>
|
||||
<small class="text-muted">Registered devices</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-muted mb-0">Catalog Items</h6>
|
||||
<h2 class="mb-0">@Model.TotalCatalogItems</h2>
|
||||
</div>
|
||||
<div class="fs-1 text-warning">🏪</div>
|
||||
</div>
|
||||
<small class="text-muted">Available items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">⚡ Quick Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a href="/admin/users" class="btn btn-primary">
|
||||
<i class="bi bi-people"></i> Manage Users
|
||||
</a>
|
||||
<a href="/admin/catalog" class="btn btn-info">
|
||||
<i class="bi bi-shop"></i> Manage Catalog
|
||||
</a>
|
||||
<a href="/admin/sessions" class="btn btn-success">
|
||||
<i class="bi bi-clock-history"></i> View Sessions
|
||||
</a>
|
||||
<a href="/admin/purchases" class="btn btn-warning">
|
||||
<i class="bi bi-cart"></i> View Purchases
|
||||
</a>
|
||||
<a href="/admin/settings" class="btn btn-secondary">
|
||||
<i class="bi bi-gear"></i> Server Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">📊 Recent Users</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Synergy ID</th>
|
||||
<th>Joined</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in Model.RecentUsers)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@user.SynergyId</code></td>
|
||||
<td><small>@user.CreatedAt.ToString("g")</small></td>
|
||||
<td>
|
||||
<a href="/admin/users?id=@user.Id" class="btn btn-sm btn-outline-primary">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/admin/users" class="btn btn-sm btn-link">View All Users →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">🔄 Active Sessions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>Expires</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var session in Model.RecentSessions)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@session.SessionId.Substring(0, 8)...</code></td>
|
||||
<td><small>@session.ExpiresAt.ToString("g")</small></td>
|
||||
<td><span class="badge bg-success">Active</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/admin/sessions" class="btn btn-sm btn-link">View All Sessions →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Info -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">ℹ️ Server Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Server URL:</dt>
|
||||
<dd class="col-sm-8"><code>@Model.ServerUrl</code></dd>
|
||||
|
||||
<dt class="col-sm-4">Platform:</dt>
|
||||
<dd class="col-sm-8">@Model.Platform</dd>
|
||||
|
||||
<dt class="col-sm-4">.NET Version:</dt>
|
||||
<dd class="col-sm-8">@Model.DotNetVersion</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Database:</dt>
|
||||
<dd class="col-sm-8">SQLite (EF Core)</dd>
|
||||
|
||||
<dt class="col-sm-4">API Endpoints:</dt>
|
||||
<dd class="col-sm-8">12 active</dd>
|
||||
|
||||
<dt class="col-sm-4">Swagger:</dt>
|
||||
<dd class="col-sm-8"><a href="/swagger" target="_blank">View API Docs</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
58
RR3CommunityServer/Pages/Admin.cshtml.cs
Normal file
58
RR3CommunityServer/Pages/Admin.cshtml.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using static RR3CommunityServer.Data.RR3DbContext;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
public class AdminModel : PageModel
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
|
||||
public AdminModel(RR3DbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public int TotalUsers { get; set; }
|
||||
public int ActiveSessions { get; set; }
|
||||
public int TotalDevices { get; set; }
|
||||
public int TotalCatalogItems { get; set; }
|
||||
public string Uptime { get; set; } = "0:00:00";
|
||||
public string ServerUrl { get; set; } = string.Empty;
|
||||
public string Platform { get; set; } = string.Empty;
|
||||
public string DotNetVersion { get; set; } = string.Empty;
|
||||
|
||||
public List<User> RecentUsers { get; set; } = new();
|
||||
public List<Session> RecentSessions { get; set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
// Get statistics
|
||||
TotalUsers = await _context.Users.CountAsync();
|
||||
TotalDevices = await _context.Devices.CountAsync();
|
||||
TotalCatalogItems = await _context.CatalogItems.CountAsync();
|
||||
ActiveSessions = await _context.Sessions
|
||||
.Where(s => s.ExpiresAt > DateTime.UtcNow)
|
||||
.CountAsync();
|
||||
|
||||
// Get recent activity
|
||||
RecentUsers = await _context.Users
|
||||
.OrderByDescending(u => u.CreatedAt)
|
||||
.Take(5)
|
||||
.ToListAsync();
|
||||
|
||||
RecentSessions = await _context.Sessions
|
||||
.Where(s => s.ExpiresAt > DateTime.UtcNow)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.Take(5)
|
||||
.ToListAsync();
|
||||
|
||||
// Server info
|
||||
var uptime = DateTime.UtcNow - System.Diagnostics.Process.GetCurrentProcess().StartTime.ToUniversalTime();
|
||||
Uptime = $"{uptime.Days}d {uptime.Hours}h {uptime.Minutes}m";
|
||||
ServerUrl = $"{Request.Scheme}://{Request.Host}";
|
||||
Platform = Environment.OSVersion.Platform.ToString();
|
||||
DotNetVersion = Environment.Version.ToString();
|
||||
}
|
||||
}
|
||||
201
RR3CommunityServer/Pages/Catalog.cshtml
Normal file
201
RR3CommunityServer/Pages/Catalog.cshtml
Normal file
@@ -0,0 +1,201 @@
|
||||
@page
|
||||
@model RR3CommunityServer.Pages.CatalogModel
|
||||
@{
|
||||
ViewData["Title"] = "Catalog Management";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1>🏪 Catalog Management</h1>
|
||||
<p class="text-muted">Manage in-game items and purchases</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
||||
<i class="bi bi-plus-circle"></i> Add New Item
|
||||
</button>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">Catalog Items (@Model.CatalogItems.Count)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Price</th>
|
||||
<th>Available</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.CatalogItems)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@item.Sku</code></td>
|
||||
<td><strong>@item.Name</strong></td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">@item.Type</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (item.Price == 0)
|
||||
{
|
||||
<span class="text-success"><strong>FREE</strong></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@item.Price.ToString("C2")</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (item.Available)
|
||||
{
|
||||
<span class="badge bg-success">✓ Yes</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">✗ No</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#editModal@(item.Id)">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
<form method="post" asp-page-handler="ToggleAvailability" class="d-inline">
|
||||
<input type="hidden" name="itemId" value="@item.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-@(item.Available ? "warning" : "success")">
|
||||
<i class="bi bi-@(item.Available ? "eye-slash" : "eye")"></i> @(item.Available ? "Disable" : "Enable")
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" asp-page-handler="Delete" class="d-inline">
|
||||
<input type="hidden" name="itemId" value="@item.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete this item?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal@(item.Id)" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" asp-page-handler="Update">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">Edit Item</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="itemId" value="@item.Id" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SKU</label>
|
||||
<input type="text" name="sku" value="@item.Sku" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" name="name" value="@item.Name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="type" class="form-select" required>
|
||||
<option value="car" selected="@(item.Type == "car")">Car</option>
|
||||
<option value="upgrade" selected="@(item.Type == "upgrade")">Upgrade</option>
|
||||
<option value="currency" selected="@(item.Type == "currency")">Currency</option>
|
||||
<option value="consumable" selected="@(item.Type == "consumable")">Consumable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Price</label>
|
||||
<input type="number" name="price" value="@item.Price" step="0.01" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="available" checked="@item.Available" class="form-check-input" id="available@(item.Id)">
|
||||
<label class="form-check-label" for="available@(item.Id)">Available for purchase</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add New Item Modal -->
|
||||
<div class="modal fade" id="addItemModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" asp-page-handler="Add">
|
||||
<div class="modal-header bg-success text-white">
|
||||
<h5 class="modal-title">Add New Item</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SKU</label>
|
||||
<input type="text" name="sku" class="form-control" placeholder="com.ea.rr3.car.porsche911" required>
|
||||
<small class="text-muted">Unique identifier (e.g., com.ea.rr3.car.name)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" name="name" class="form-control" placeholder="Porsche 911 GT3" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="type" class="form-select" required>
|
||||
<option value="car">Car</option>
|
||||
<option value="upgrade">Upgrade</option>
|
||||
<option value="currency">Currency</option>
|
||||
<option value="consumable">Consumable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Price</label>
|
||||
<input type="number" name="price" step="0.01" value="0.00" class="form-control" required>
|
||||
<small class="text-muted">Set to 0 for free items</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="available" checked class="form-check-input" id="availableNew">
|
||||
<label class="form-check-label" for="availableNew">Available for purchase</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-success">Add Item</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
84
RR3CommunityServer/Pages/Catalog.cshtml.cs
Normal file
84
RR3CommunityServer/Pages/Catalog.cshtml.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using static RR3CommunityServer.Data.RR3DbContext;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
public class CatalogModel : PageModel
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
|
||||
public CatalogModel(RR3DbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public List<CatalogItem> CatalogItems { get; set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
CatalogItems = await _context.CatalogItems
|
||||
.OrderBy(c => c.Type)
|
||||
.ThenBy(c => c.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAddAsync(string sku, string name, string type, decimal price, bool available)
|
||||
{
|
||||
var item = new CatalogItem
|
||||
{
|
||||
Sku = sku,
|
||||
Name = name,
|
||||
Type = type,
|
||||
Price = price,
|
||||
Available = available
|
||||
};
|
||||
|
||||
_context.CatalogItems.Add(item);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostUpdateAsync(int itemId, string sku, string name, string type, decimal price, bool available)
|
||||
{
|
||||
var item = await _context.CatalogItems.FindAsync(itemId);
|
||||
if (item != null)
|
||||
{
|
||||
item.Sku = sku;
|
||||
item.Name = name;
|
||||
item.Type = type;
|
||||
item.Price = price;
|
||||
item.Available = available;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostToggleAvailabilityAsync(int itemId)
|
||||
{
|
||||
var item = await _context.CatalogItems.FindAsync(itemId);
|
||||
if (item != null)
|
||||
{
|
||||
item.Available = !item.Available;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(int itemId)
|
||||
{
|
||||
var item = await _context.CatalogItems.FindAsync(itemId);
|
||||
if (item != null)
|
||||
{
|
||||
_context.CatalogItems.Remove(item);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
185
RR3CommunityServer/Pages/Purchases.cshtml
Normal file
185
RR3CommunityServer/Pages/Purchases.cshtml
Normal file
@@ -0,0 +1,185 @@
|
||||
@page
|
||||
@model RR3CommunityServer.Pages.PurchasesModel
|
||||
@{
|
||||
ViewData["Title"] = "Purchase History";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1>🛒 Purchase History</h1>
|
||||
<p class="text-muted">View all in-game purchases</p>
|
||||
</div>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Purchases.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No purchases yet. Purchases will appear here when players buy items.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Statistics -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Purchases</h6>
|
||||
<h2 class="text-primary">@Model.Purchases.Count</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-success">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Approved</h6>
|
||||
<h2 class="text-success">@Model.Purchases.Count(p => p.Status == "approved")</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-info">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Value</h6>
|
||||
<h2 class="text-info">@Model.TotalValue.ToString("C2")</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purchase List -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Purchases</h5>
|
||||
<form method="get" class="d-flex gap-2">
|
||||
<input type="text" name="search" value="@Model.SearchQuery" class="form-control form-control-sm" placeholder="Search by SKU or User...">
|
||||
<button type="submit" class="btn btn-sm btn-dark">Search</button>
|
||||
@if (!string.IsNullOrEmpty(Model.SearchQuery))
|
||||
{
|
||||
<a href="/admin/purchases" class="btn btn-sm btn-outline-dark">Clear</a>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>User</th>
|
||||
<th>SKU</th>
|
||||
<th>Price</th>
|
||||
<th>Status</th>
|
||||
<th>Purchase Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var purchase in Model.Purchases)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@purchase.Id</strong></td>
|
||||
<td>
|
||||
@if (purchase.UserId.HasValue)
|
||||
{
|
||||
<code>User @purchase.UserId</code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Unknown</span>
|
||||
}
|
||||
</td>
|
||||
<td><code>@purchase.Sku</code></td>
|
||||
<td>
|
||||
@if (purchase.Price == 0)
|
||||
{
|
||||
<span class="text-success"><strong>FREE</strong></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@purchase.Price.ToString("C2")</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (purchase.Status == "approved")
|
||||
{
|
||||
<span class="badge bg-success">✓ Approved</span>
|
||||
}
|
||||
else if (purchase.Status == "pending")
|
||||
{
|
||||
<span class="badge bg-warning">⏳ Pending</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">✗ @purchase.Status</span>
|
||||
}
|
||||
</td>
|
||||
<td>@purchase.PurchaseDate.ToString("g")</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" data-bs-toggle="modal" data-bs-target="#purchaseModal@(purchase.Id)">
|
||||
<i class="bi bi-eye"></i> Details
|
||||
</button>
|
||||
<form method="post" asp-page-handler="Delete" class="d-inline">
|
||||
<input type="hidden" name="purchaseId" value="@purchase.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete this purchase record?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Purchase Details Modal -->
|
||||
<div class="modal fade" id="purchaseModal@(purchase.Id)" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-warning">
|
||||
<h5 class="modal-title">Purchase Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Purchase ID:</dt>
|
||||
<dd class="col-sm-9">@purchase.Id</dd>
|
||||
|
||||
<dt class="col-sm-3">User ID:</dt>
|
||||
<dd class="col-sm-9">@(purchase.UserId?.ToString() ?? "N/A")</dd>
|
||||
|
||||
<dt class="col-sm-3">SKU:</dt>
|
||||
<dd class="col-sm-9"><code>@purchase.Sku</code></dd>
|
||||
|
||||
<dt class="col-sm-3">Price:</dt>
|
||||
<dd class="col-sm-9">@purchase.Price.ToString("C2")</dd>
|
||||
|
||||
<dt class="col-sm-3">Status:</dt>
|
||||
<dd class="col-sm-9">
|
||||
<span class="badge bg-@(purchase.Status == "approved" ? "success" : "warning")">
|
||||
@purchase.Status
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">Purchase Date:</dt>
|
||||
<dd class="col-sm-9">@purchase.PurchaseDate.ToString("F")</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
52
RR3CommunityServer/Pages/Purchases.cshtml.cs
Normal file
52
RR3CommunityServer/Pages/Purchases.cshtml.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using static RR3CommunityServer.Data.RR3DbContext;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
public class PurchasesModel : PageModel
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
|
||||
public PurchasesModel(RR3DbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public List<Purchase> Purchases { get; set; } = new();
|
||||
public decimal TotalValue { get; set; }
|
||||
public string? SearchQuery { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? search)
|
||||
{
|
||||
SearchQuery = search;
|
||||
|
||||
var query = _context.Purchases.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
{
|
||||
query = query.Where(p => p.Sku.Contains(search) ||
|
||||
(p.UserId != null && p.UserId.ToString()!.Contains(search)));
|
||||
}
|
||||
|
||||
Purchases = await query
|
||||
.OrderByDescending(p => p.PurchaseDate)
|
||||
.ToListAsync();
|
||||
|
||||
TotalValue = Purchases.Sum(p => p.Price);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(int purchaseId)
|
||||
{
|
||||
var purchase = await _context.Purchases.FindAsync(purchaseId);
|
||||
if (purchase != null)
|
||||
{
|
||||
_context.Purchases.Remove(purchase);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
160
RR3CommunityServer/Pages/Sessions.cshtml
Normal file
160
RR3CommunityServer/Pages/Sessions.cshtml
Normal file
@@ -0,0 +1,160 @@
|
||||
@page
|
||||
@model RR3CommunityServer.Pages.SessionsModel
|
||||
@{
|
||||
ViewData["Title"] = "Session Management";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1>🔄 Session Management</h1>
|
||||
<p class="text-muted">Monitor active and expired sessions</p>
|
||||
</div>
|
||||
<div>
|
||||
<form method="post" asp-page-handler="CleanupExpired" class="d-inline">
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-trash3"></i> Cleanup Expired
|
||||
</button>
|
||||
</form>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-success">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Active Sessions</h6>
|
||||
<h2 class="text-success">@Model.ActiveSessions.Count</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Expired Sessions</h6>
|
||||
<h2 class="text-danger">@Model.ExpiredSessions.Count</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-info">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Sessions</h6>
|
||||
<h2 class="text-info">@Model.AllSessions.Count</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Sessions -->
|
||||
@if (Model.ActiveSessions.Any())
|
||||
{
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">🟢 Active Sessions (@Model.ActiveSessions.Count)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>User ID</th>
|
||||
<th>Device ID</th>
|
||||
<th>Created</th>
|
||||
<th>Expires</th>
|
||||
<th>Time Left</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var session in Model.ActiveSessions)
|
||||
{
|
||||
var timeLeft = session.ExpiresAt - DateTime.UtcNow;
|
||||
<tr>
|
||||
<td><code>@session.SessionId.Substring(0, 12)...</code></td>
|
||||
<td>@session.UserId</td>
|
||||
<td><code>@session.DeviceId.Substring(0, 12)...</code></td>
|
||||
<td>@session.CreatedAt.ToString("g")</td>
|
||||
<td>@session.ExpiresAt.ToString("g")</td>
|
||||
<td>
|
||||
<span class="badge bg-success">
|
||||
@((int)timeLeft.TotalHours)h @timeLeft.Minutes)m
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" asp-page-handler="Delete" class="d-inline">
|
||||
<input type="hidden" name="sessionId" value="@session.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Terminate this session?')">
|
||||
<i class="bi bi-x-circle"></i> Terminate
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Expired Sessions -->
|
||||
@if (Model.ExpiredSessions.Any())
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h5 class="mb-0">⚫ Expired Sessions (@Model.ExpiredSessions.Count)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>User ID</th>
|
||||
<th>Device ID</th>
|
||||
<th>Created</th>
|
||||
<th>Expired</th>
|
||||
<th>Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var session in Model.ExpiredSessions.Take(20))
|
||||
{
|
||||
var duration = session.ExpiresAt - session.CreatedAt;
|
||||
<tr class="text-muted">
|
||||
<td><code>@session.SessionId.Substring(0, 12)...</code></td>
|
||||
<td>@session.UserId</td>
|
||||
<td><code>@session.DeviceId.Substring(0, 12)...</code></td>
|
||||
<td>@session.CreatedAt.ToString("g")</td>
|
||||
<td>@session.ExpiresAt.ToString("g")</td>
|
||||
<td>@((int)duration.TotalHours)h</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (Model.ExpiredSessions.Count > 20)
|
||||
{
|
||||
<div class="text-muted text-center">
|
||||
<small>Showing 20 of @Model.ExpiredSessions.Count expired sessions</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.AllSessions.Any())
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No sessions yet. Sessions will appear when players connect to the server.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
56
RR3CommunityServer/Pages/Sessions.cshtml.cs
Normal file
56
RR3CommunityServer/Pages/Sessions.cshtml.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using static RR3CommunityServer.Data.RR3DbContext;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
public class SessionsModel : PageModel
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
|
||||
public SessionsModel(RR3DbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public List<Session> AllSessions { get; set; } = new();
|
||||
public List<Session> ActiveSessions { get; set; } = new();
|
||||
public List<Session> ExpiredSessions { get; set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
AllSessions = await _context.Sessions
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
ActiveSessions = AllSessions.Where(s => s.ExpiresAt > now).ToList();
|
||||
ExpiredSessions = AllSessions.Where(s => s.ExpiresAt <= now).ToList();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(int sessionId)
|
||||
{
|
||||
var session = await _context.Sessions.FindAsync(sessionId);
|
||||
if (session != null)
|
||||
{
|
||||
_context.Sessions.Remove(session);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostCleanupExpiredAsync()
|
||||
{
|
||||
var expiredSessions = await _context.Sessions
|
||||
.Where(s => s.ExpiresAt <= DateTime.UtcNow)
|
||||
.ToListAsync();
|
||||
|
||||
_context.Sessions.RemoveRange(expiredSessions);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
213
RR3CommunityServer/Pages/Settings.cshtml
Normal file
213
RR3CommunityServer/Pages/Settings.cshtml
Normal file
@@ -0,0 +1,213 @@
|
||||
@page
|
||||
@model RR3CommunityServer.Pages.SettingsModel
|
||||
@{
|
||||
ViewData["Title"] = "Server Settings";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1>⚙️ Server Settings</h1>
|
||||
<p class="text-muted">Configure server behavior and options</p>
|
||||
</div>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Configuration -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">🌐 Server Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Server URL:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<code>@Model.ServerUrl</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('@Model.ServerUrl')">
|
||||
<i class="bi bi-clipboard"></i> Copy
|
||||
</button>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Director Endpoint:</dt>
|
||||
<dd class="col-sm-8"><code>@Model.ServerUrl/synergy/director</code></dd>
|
||||
|
||||
<dt class="col-sm-4">Database:</dt>
|
||||
<dd class="col-sm-8">SQLite (rr3community.db)</dd>
|
||||
|
||||
<dt class="col-sm-4">Session Timeout:</dt>
|
||||
<dd class="col-sm-8">24 hours</dd>
|
||||
|
||||
<dt class="col-sm-4">Auto-Approve Purchases:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-success">✓ Enabled</span>
|
||||
<small class="text-muted d-block">All purchases auto-approved for community servers</small>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- APK Configuration -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">📱 APK Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="mb-3">To connect game clients to this server:</h6>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Method 1: Use the automated script</strong>
|
||||
<pre class="mb-2 mt-2"><code>.\RR3-Community-Mod.ps1 -ServerUrl "@Model.ServerUrl"</code></pre>
|
||||
<small>Located in: <code>E:\rr3\RR3-Community-Mod.ps1</code></small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-secondary">
|
||||
<strong>Method 2: Manual AndroidManifest.xml modification</strong>
|
||||
<ol class="small mb-0 mt-2">
|
||||
<li>Decompile APK with APKTool</li>
|
||||
<li>Edit AndroidManifest.xml:
|
||||
<ul>
|
||||
<li>Change <code>com.ea.nimble.configuration</code> from "live" to "custom"</li>
|
||||
<li>Add metadata: <code>NimbleCustomizedSynergyServerEndpointUrl</code> = <code>@Model.ServerUrl</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Recompile and sign APK</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<p class="mb-0">
|
||||
<a href="file:///E:/rr3/APK_MODIFICATION_GUIDE.md" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-book"></i> View Full Guide
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Information -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">💻 System Info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="mb-0">
|
||||
<dt>Operating System</dt>
|
||||
<dd><code>@Model.Platform</code></dd>
|
||||
|
||||
<dt>.NET Version</dt>
|
||||
<dd><code>@Model.DotNetVersion</code></dd>
|
||||
|
||||
<dt>ASP.NET Core</dt>
|
||||
<dd><code>@Model.AspNetVersion</code></dd>
|
||||
|
||||
<dt>Server Uptime</dt>
|
||||
<dd><strong>@Model.Uptime</strong></dd>
|
||||
|
||||
<dt>Process ID</dt>
|
||||
<dd><code>@Model.ProcessId</code></dd>
|
||||
|
||||
<dt>Working Memory</dt>
|
||||
<dd><code>@Model.MemoryUsage MB</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h5 class="mb-0">🔗 Quick Links</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="/swagger" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="bi bi-code-slash"></i> Swagger API Docs
|
||||
</a>
|
||||
<a href="file:///E:/rr3/NETWORK_COMMUNICATION_ANALYSIS.md" class="btn btn-outline-info">
|
||||
<i class="bi bi-file-text"></i> Protocol Documentation
|
||||
</a>
|
||||
<a href="file:///E:/rr3/RR3CommunityServer/README.md" class="btn btn-outline-success">
|
||||
<i class="bi bi-journal"></i> Server README
|
||||
</a>
|
||||
<a href="file:///E:/rr3/PROJECT_INDEX.md" class="btn btn-outline-warning">
|
||||
<i class="bi bi-folder"></i> Project Index
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Management -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<h5 class="mb-0">🗄️ Database Management</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-primary">@Model.DbStats.Users</h3>
|
||||
<p class="mb-0 text-muted">Users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-success">@Model.DbStats.Devices</h3>
|
||||
<p class="mb-0 text-muted">Devices</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-info">@Model.DbStats.Sessions</h3>
|
||||
<p class="mb-0 text-muted">Sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-warning">@Model.DbStats.Purchases</h3>
|
||||
<p class="mb-0 text-muted">Purchases</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h6>⚠️ Danger Zone</h6>
|
||||
<div class="alert alert-danger">
|
||||
<form method="post" asp-page-handler="ResetDatabase" onsubmit="return confirm('This will DELETE ALL DATA and reset the database. Are you sure?')">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-exclamation-triangle"></i> Reset Database (Delete All Data)
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('Copied to clipboard: ' + text);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
74
RR3CommunityServer/Pages/Settings.cshtml.cs
Normal file
74
RR3CommunityServer/Pages/Settings.cshtml.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
public class SettingsModel : PageModel
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
|
||||
public SettingsModel(RR3DbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public string ServerUrl { get; set; } = string.Empty;
|
||||
public string Platform { get; set; } = string.Empty;
|
||||
public string DotNetVersion { get; set; } = string.Empty;
|
||||
public string AspNetVersion { get; set; } = string.Empty;
|
||||
public string Uptime { get; set; } = string.Empty;
|
||||
public int ProcessId { get; set; }
|
||||
public long MemoryUsage { get; set; }
|
||||
public DatabaseStats DbStats { get; set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
ServerUrl = $"{Request.Scheme}://{Request.Host}";
|
||||
Platform = Environment.OSVersion.ToString();
|
||||
DotNetVersion = Environment.Version.ToString();
|
||||
AspNetVersion = typeof(IApplicationBuilder).Assembly.GetName().Version?.ToString() ?? "Unknown";
|
||||
|
||||
var process = System.Diagnostics.Process.GetCurrentProcess();
|
||||
var uptime = DateTime.UtcNow - process.StartTime.ToUniversalTime();
|
||||
Uptime = $"{uptime.Days}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s";
|
||||
ProcessId = process.Id;
|
||||
MemoryUsage = process.WorkingSet64 / 1024 / 1024; // Convert to MB
|
||||
|
||||
// Get database stats
|
||||
DbStats = new DatabaseStats
|
||||
{
|
||||
Users = await _context.Users.CountAsync(),
|
||||
Devices = await _context.Devices.CountAsync(),
|
||||
Sessions = await _context.Sessions.CountAsync(),
|
||||
Purchases = await _context.Purchases.CountAsync()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostResetDatabaseAsync()
|
||||
{
|
||||
// Delete all data
|
||||
_context.Purchases.RemoveRange(_context.Purchases);
|
||||
_context.Sessions.RemoveRange(_context.Sessions);
|
||||
_context.Users.RemoveRange(_context.Users);
|
||||
_context.Devices.RemoveRange(_context.Devices);
|
||||
_context.CatalogItems.RemoveRange(_context.CatalogItems);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Re-seed catalog
|
||||
await _context.Database.EnsureDeletedAsync();
|
||||
await _context.Database.EnsureCreatedAsync();
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
|
||||
public class DatabaseStats
|
||||
{
|
||||
public int Users { get; set; }
|
||||
public int Devices { get; set; }
|
||||
public int Sessions { get; set; }
|
||||
public int Purchases { get; set; }
|
||||
}
|
||||
111
RR3CommunityServer/Pages/Users.cshtml
Normal file
111
RR3CommunityServer/Pages/Users.cshtml
Normal file
@@ -0,0 +1,111 @@
|
||||
@page
|
||||
@model RR3CommunityServer.Pages.UsersModel
|
||||
@{
|
||||
ViewData["Title"] = "User Management";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1>👥 User Management</h1>
|
||||
<p class="text-muted">Manage all registered users</p>
|
||||
</div>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Users.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No users registered yet. Users will appear here when players connect to your server.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Users (@Model.Users.Count)</h5>
|
||||
<form method="get" class="d-flex gap-2">
|
||||
<input type="text" name="search" value="@Model.SearchQuery" class="form-control form-control-sm" placeholder="Search by Synergy ID...">
|
||||
<button type="submit" class="btn btn-sm btn-light">Search</button>
|
||||
@if (!string.IsNullOrEmpty(Model.SearchQuery))
|
||||
{
|
||||
<a href="/admin/users" class="btn btn-sm btn-outline-light">Clear</a>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Synergy ID</th>
|
||||
<th>Device ID</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in Model.Users)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@user.Id</strong></td>
|
||||
<td><code>@user.SynergyId</code></td>
|
||||
<td><code>@user.DeviceId</code></td>
|
||||
<td>@user.CreatedAt.ToString("g")</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" data-bs-toggle="modal" data-bs-target="#userModal@(user.Id)">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</button>
|
||||
<form method="post" asp-page-handler="Delete" class="d-inline">
|
||||
<input type="hidden" name="userId" value="@user.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete this user?')">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- User Details Modal -->
|
||||
<div class="modal fade" id="userModal@(user.Id)" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">User Details</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">User ID:</dt>
|
||||
<dd class="col-sm-9">@user.Id</dd>
|
||||
|
||||
<dt class="col-sm-3">Synergy ID:</dt>
|
||||
<dd class="col-sm-9"><code>@user.SynergyId</code></dd>
|
||||
|
||||
<dt class="col-sm-3">Device ID:</dt>
|
||||
<dd class="col-sm-9"><code>@user.DeviceId</code></dd>
|
||||
|
||||
<dt class="col-sm-3">Created:</dt>
|
||||
<dd class="col-sm-9">@user.CreatedAt.ToString("F")</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
48
RR3CommunityServer/Pages/Users.cshtml.cs
Normal file
48
RR3CommunityServer/Pages/Users.cshtml.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using static RR3CommunityServer.Data.RR3DbContext;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
public class UsersModel : PageModel
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
|
||||
public UsersModel(RR3DbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public List<User> Users { get; set; } = new();
|
||||
public string? SearchQuery { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? search)
|
||||
{
|
||||
SearchQuery = search;
|
||||
|
||||
var query = _context.Users.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
{
|
||||
query = query.Where(u => u.SynergyId.Contains(search) || u.DeviceId.Contains(search));
|
||||
}
|
||||
|
||||
Users = await query
|
||||
.OrderByDescending(u => u.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(int userId)
|
||||
{
|
||||
var user = await _context.Users.FindAsync(userId);
|
||||
if (user != null)
|
||||
{
|
||||
_context.Users.Remove(user);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
130
RR3CommunityServer/Pages/_Layout.cshtml
Normal file
130
RR3CommunityServer/Pages/_Layout.cshtml
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - RR3 Community Server</title>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--rr3-primary: #e63946;
|
||||
--rr3-dark: #1d3557;
|
||||
--rr3-light: #f1faee;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: linear-gradient(135deg, var(--rr3-dark) 0%, #2a4d7a 100%);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
border-radius: 12px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--rr3-primary);
|
||||
border-color: var(--rr3-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #d62839;
|
||||
border-color: #d62839;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(230, 57, 70, 0.05);
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark mb-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/admin">
|
||||
<span class="fs-3">🏎️</span>
|
||||
<strong>RR3 Community Server</strong>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin">
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/users">
|
||||
<i class="bi bi-people"></i> Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/catalog">
|
||||
<i class="bi bi-shop"></i> Catalog
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/sessions">
|
||||
<i class="bi bi-clock-history"></i> Sessions
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/purchases">
|
||||
<i class="bi bi-cart"></i> Purchases
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/swagger" target="_blank">
|
||||
<i class="bi bi-code-slash"></i> API
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-5 py-4 bg-light">
|
||||
<div class="container text-center text-muted">
|
||||
<p class="mb-0">
|
||||
<strong>RR3 Community Server</strong> - Open Source Game Server
|
||||
</p>
|
||||
<small>Made for game preservation and educational purposes</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap 5 JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
3
RR3CommunityServer/Pages/_ViewStart.cshtml
Normal file
3
RR3CommunityServer/Pages/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
Reference in New Issue
Block a user