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:
2026-02-17 22:02:12 -08:00
commit 0a327f3a8b
187 changed files with 9282 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Mvc;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("director/api/android")]
public class DirectorController : ControllerBase
{
private readonly ILogger<DirectorController> _logger;
private readonly IConfiguration _configuration;
public DirectorController(ILogger<DirectorController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
[HttpGet("getDirectionByPackage")]
public ActionResult<SynergyResponse<DirectorResponse>> GetDirection([FromQuery] string packageName)
{
_logger.LogInformation("Director request for package: {Package}", packageName);
var baseUrl = $"{Request.Scheme}://{Request.Host}";
var response = new SynergyResponse<DirectorResponse>
{
resultCode = 0,
message = "Success",
data = new DirectorResponse
{
serverUrls = new Dictionary<string, string>
{
{ "synergy.product", baseUrl },
{ "synergy.drm", baseUrl },
{ "synergy.user", baseUrl },
{ "synergy.tracking", baseUrl },
{ "synergy.s2s", baseUrl },
{ "nexus.portal", baseUrl },
{ "ens.url", baseUrl }
},
environment = "COMMUNITY",
version = "1.0.0"
}
};
return Ok(response);
}
}

View File

@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Mvc;
using RR3CommunityServer.Models;
using RR3CommunityServer.Services;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("drm/api")]
public class DrmController : ControllerBase
{
private readonly IDrmService _drmService;
private readonly ILogger<DrmController> _logger;
public DrmController(IDrmService drmService, ILogger<DrmController> logger)
{
_drmService = drmService;
_logger = logger;
}
[HttpGet("core/getNonce")]
public async Task<ActionResult<SynergyResponse<DrmNonceResponse>>> GetNonce()
{
_logger.LogInformation("GetNonce request");
var nonce = await _drmService.GenerateNonce();
var response = new SynergyResponse<DrmNonceResponse>
{
resultCode = 0,
message = "Success",
data = new DrmNonceResponse
{
nonce = nonce,
expiresAt = DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds()
}
};
return Ok(response);
}
[HttpGet("core/getPurchasedItems")]
public async Task<ActionResult<SynergyResponse<List<PurchasedItem>>>> GetPurchasedItems()
{
var synergyId = HttpContext.Items["EAM-USER-ID"]?.ToString() ?? "default";
_logger.LogInformation("GetPurchasedItems for user: {SynergyId}", synergyId);
var purchases = await _drmService.GetPurchasedItems(synergyId);
var response = new SynergyResponse<List<PurchasedItem>>
{
resultCode = 0,
message = "Success",
data = purchases
};
return Ok(response);
}
[HttpPost("android/verifyAndRecordPurchase")]
public async Task<ActionResult<SynergyResponse<object>>> VerifyPurchase([FromBody] PurchaseVerificationRequest request)
{
var synergyId = HttpContext.Items["EAM-USER-ID"]?.ToString() ?? "default";
_logger.LogInformation("VerifyAndRecordPurchase: user={User}, sku={Sku}", synergyId, request.sku);
var verified = await _drmService.VerifyAndRecordPurchase(synergyId, request);
var response = new SynergyResponse<object>
{
resultCode = verified ? 0 : -1,
message = verified ? "Purchase verified" : "Purchase verification failed",
data = new { verified = verified }
};
return Ok(response);
}
}

View File

@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Mvc;
using RR3CommunityServer.Models;
using RR3CommunityServer.Services;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("product/api/core")]
public class ProductController : ControllerBase
{
private readonly ICatalogService _catalogService;
private readonly ILogger<ProductController> _logger;
public ProductController(ICatalogService catalogService, ILogger<ProductController> logger)
{
_catalogService = catalogService;
_logger = logger;
}
[HttpGet("getAvailableItems")]
public async Task<ActionResult<SynergyResponse<List<CatalogItem>>>> GetAvailableItems()
{
_logger.LogInformation("GetAvailableItems request");
var items = await _catalogService.GetAvailableItems();
var response = new SynergyResponse<List<CatalogItem>>
{
resultCode = 0,
message = "Success",
data = items
};
return Ok(response);
}
[HttpGet("getMTXGameCategories")]
public async Task<ActionResult<SynergyResponse<List<CatalogCategory>>>> GetCategories()
{
_logger.LogInformation("GetMTXGameCategories request");
var categories = await _catalogService.GetCategories();
var response = new SynergyResponse<List<CatalogCategory>>
{
resultCode = 0,
message = "Success",
data = categories
};
return Ok(response);
}
[HttpPost("getDownloadItemUrl")]
public async Task<ActionResult<SynergyResponse<object>>> GetDownloadUrl([FromBody] Dictionary<string, string> request)
{
var itemId = request.GetValueOrDefault("itemId", "");
_logger.LogInformation("GetDownloadItemUrl: {ItemId}", itemId);
var url = await _catalogService.GetDownloadUrl(itemId);
var response = new SynergyResponse<object>
{
resultCode = 0,
message = "Success",
data = new { downloadUrl = url }
};
return Ok(response);
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Mvc;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("tracking/api/core")]
public class TrackingController : ControllerBase
{
private readonly ILogger<TrackingController> _logger;
public TrackingController(ILogger<TrackingController> logger)
{
_logger = logger;
}
[HttpPost("logEvent")]
public ActionResult<SynergyResponse<object>> LogEvent([FromBody] TrackingEvent trackingEvent)
{
_logger.LogInformation("Tracking Event: {EventType} at {Timestamp}",
trackingEvent.eventType,
trackingEvent.timestamp);
// For community server, we just log and accept all events
var response = new SynergyResponse<object>
{
resultCode = 0,
message = "Event logged",
data = new { received = true }
};
return Ok(response);
}
[HttpPost("logEvents")]
public ActionResult<SynergyResponse<object>> LogEvents([FromBody] List<TrackingEvent> events)
{
_logger.LogInformation("Tracking Batch: {Count} events", events.Count);
var response = new SynergyResponse<object>
{
resultCode = 0,
message = $"{events.Count} events logged",
data = new { received = events.Count }
};
return Ok(response);
}
}

View File

@@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Mvc;
using RR3CommunityServer.Models;
using RR3CommunityServer.Services;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("user/api/android")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
private readonly ISessionService _sessionService;
private readonly ILogger<UserController> _logger;
public UserController(IUserService userService, ISessionService sessionService, ILogger<UserController> logger)
{
_userService = userService;
_sessionService = sessionService;
_logger = logger;
}
[HttpGet("getDeviceID")]
public async Task<ActionResult<SynergyResponse<DeviceIdResponse>>> GetDeviceId(
[FromQuery] string? deviceId,
[FromQuery] string hardwareId = "")
{
_logger.LogInformation("GetDeviceID request: existing={Existing}, hardware={Hardware}", deviceId, hardwareId);
var newDeviceId = await _userService.GetOrCreateDeviceId(deviceId, hardwareId);
var synergyId = await _userService.GetOrCreateSynergyId(newDeviceId);
var sessionId = await _sessionService.CreateSession(synergyId);
var response = new SynergyResponse<DeviceIdResponse>
{
resultCode = 0,
message = "Success",
data = new DeviceIdResponse
{
deviceId = newDeviceId,
synergyId = synergyId,
timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
}
};
return Ok(response);
}
[HttpGet("validateDeviceID")]
public async Task<ActionResult<SynergyResponse<object>>> ValidateDeviceId([FromQuery] string deviceId)
{
_logger.LogInformation("ValidateDeviceID: {DeviceId}", deviceId);
var result = await _userService.ValidateDeviceId(deviceId);
var response = new SynergyResponse<object>
{
resultCode = result == "valid" ? 0 : -1,
message = result == "valid" ? "Device validated" : "Device not found",
data = new { status = result }
};
return Ok(response);
}
[HttpGet("getAnonUid")]
public async Task<ActionResult<SynergyResponse<AnonUidResponse>>> GetAnonUid()
{
_logger.LogInformation("GetAnonUid request");
var anonUid = await _userService.GetOrCreateAnonUid();
var response = new SynergyResponse<AnonUidResponse>
{
resultCode = 0,
message = "Success",
data = new AnonUidResponse
{
anonUid = anonUid,
expiresAt = DateTimeOffset.UtcNow.AddDays(30).ToUnixTimeSeconds()
}
};
return Ok(response);
}
}

View File

@@ -0,0 +1,106 @@
using Microsoft.EntityFrameworkCore;
namespace RR3CommunityServer.Data;
public class RR3DbContext : DbContext
{
public RR3DbContext(DbContextOptions<RR3DbContext> options) : base(options) { }
public DbSet<Device> Devices { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Session> Sessions { get; set; }
public DbSet<Purchase> Purchases { get; set; }
public DbSet<CatalogItem> CatalogItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Seed some default catalog items
modelBuilder.Entity<CatalogItem>().HasData(
new CatalogItem
{
Id = 1,
Sku = "com.ea.rr3.gold_1000",
Name = "1000 Gold",
Type = "currency",
Price = 0.99m,
Available = true
},
new CatalogItem
{
Id = 2,
Sku = "com.ea.rr3.car_tier1",
Name = "Starter Car",
Type = "car",
Price = 0m,
Available = true
},
new CatalogItem
{
Id = 3,
Sku = "com.ea.rr3.upgrade_engine",
Name = "Engine Upgrade",
Type = "upgrade",
Price = 4.99m,
Available = true
}
);
}
}
// Database entities
public class Device
{
public int Id { get; set; }
public string DeviceId { get; set; } = string.Empty;
public string HardwareId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastSeenAt { get; set; } = DateTime.UtcNow;
}
public class User
{
public int Id { get; set; }
public string SynergyId { get; set; } = string.Empty;
public string? DeviceId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string? Nickname { get; set; }
}
public class Session
{
public int Id { get; set; }
public string SessionId { get; set; } = string.Empty;
public string? SynergyId { get; set; }
public string DeviceId { get; set; } = string.Empty;
public int? UserId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime ExpiresAt { get; set; }
}
public class Purchase
{
public int Id { get; set; }
public string SynergyId { get; set; } = string.Empty;
public string ItemId { get; set; } = string.Empty;
public string Sku { get; set; } = string.Empty;
public string OrderId { get; set; } = string.Empty;
public DateTime PurchaseTime { get; set; } = DateTime.UtcNow;
public string Token { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Status { get; set; } = "approved";
// For web panel display
public int? UserId { get; set; }
public DateTime PurchaseDate => PurchaseTime;
}
public class CatalogItem
{
public int Id { get; set; }
public string Sku { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public decimal Price { get; set; }
public bool Available { get; set; } = true;
}

View File

@@ -0,0 +1,76 @@
namespace RR3CommunityServer.Middleware;
public class SynergyHeadersMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SynergyHeadersMiddleware> _logger;
public SynergyHeadersMiddleware(RequestDelegate next, ILogger<SynergyHeadersMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Log incoming Synergy headers
var sessionId = context.Request.Headers["EAM-SESSION"].FirstOrDefault();
var userId = context.Request.Headers["EAM-USER-ID"].FirstOrDefault();
var sellId = context.Request.Headers["EA-SELL-ID"].FirstOrDefault();
var sdkVersion = context.Request.Headers["SDK-VERSION"].FirstOrDefault();
_logger.LogInformation(
"Synergy Request: Path={Path}, Session={Session}, User={User}, Sell={Sell}, SDK={SDK}",
context.Request.Path,
sessionId ?? "none",
userId ?? "none",
sellId ?? "none",
sdkVersion ?? "none"
);
// Store in context for controllers
context.Items["EAM-SESSION"] = sessionId;
context.Items["EAM-USER-ID"] = userId;
context.Items["EA-SELL-ID"] = sellId;
await _next(context);
}
}
public class SessionValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SessionValidationMiddleware> _logger;
// Paths that don't require session validation
private static readonly HashSet<string> PublicPaths = new()
{
"/director/api/android/getDirectionByPackage",
"/user/api/android/getDeviceID",
"/user/api/android/getAnonUid",
"/swagger",
"/health"
};
public SessionValidationMiddleware(RequestDelegate next, ILogger<SessionValidationMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value ?? "";
// Skip validation for public paths
if (PublicPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
{
await _next(context);
return;
}
// For now, allow all requests (lenient for community server)
// In production, validate session here
await _next(context);
}
}

View File

@@ -0,0 +1,90 @@
namespace RR3CommunityServer.Models;
// Standard Synergy API response wrapper
public class SynergyResponse<T>
{
public int resultCode { get; set; } = 0; // 0 = success, negative = error
public string? message { get; set; }
public T? data { get; set; }
}
// User models
public class DeviceIdResponse
{
public string deviceId { get; set; } = string.Empty;
public string synergyId { get; set; } = string.Empty;
public long timestamp { get; set; }
}
public class AnonUidResponse
{
public string anonUid { get; set; } = string.Empty;
public long expiresAt { get; set; }
}
// Product/Catalog models
public class CatalogItem
{
public string itemId { get; set; } = string.Empty;
public string sku { get; set; } = string.Empty;
public string name { get; set; } = string.Empty;
public string description { get; set; } = string.Empty;
public string category { get; set; } = string.Empty;
public decimal price { get; set; }
public string currency { get; set; } = "USD";
public Dictionary<string, object> metadata { get; set; } = new();
}
public class CatalogCategory
{
public string categoryId { get; set; } = string.Empty;
public string name { get; set; } = string.Empty;
public List<string> itemIds { get; set; } = new();
}
// DRM models
public class DrmNonceResponse
{
public string nonce { get; set; } = string.Empty;
public long expiresAt { get; set; }
}
public class PurchasedItem
{
public string itemId { get; set; } = string.Empty;
public string sku { get; set; } = string.Empty;
public string orderId { get; set; } = string.Empty;
public long purchaseTime { get; set; }
public string token { get; set; } = string.Empty;
}
public class PurchaseVerificationRequest
{
public string receipt { get; set; } = string.Empty;
public string signature { get; set; } = string.Empty;
public string sku { get; set; } = string.Empty;
public string orderId { get; set; } = string.Empty;
}
// Tracking models
public class TrackingEvent
{
public string eventType { get; set; } = string.Empty;
public long timestamp { get; set; }
public Dictionary<string, object> properties { get; set; } = new();
}
// Director/Service Discovery
public class DirectorResponse
{
public Dictionary<string, string> serverUrls { get; set; } = new()
{
{ "synergy.product", "https://localhost:5001" },
{ "synergy.drm", "https://localhost:5001" },
{ "synergy.user", "https://localhost:5001" },
{ "synergy.tracking", "https://localhost:5001" },
{ "synergy.s2s", "https://localhost:5001" }
};
public string environment { get; set; } = "COMMUNITY";
public string version { get; set; } = "1.0.0";
}

View 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>

View 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();
}
}

View 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>

View 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();
}
}

View 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>

View 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();
}
}

View 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>

View 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();
}
}

View 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>
}

View 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; }
}

View 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>

View 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();
}
}

View 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>

View File

@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@@ -0,0 +1,77 @@
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using RR3CommunityServer.Services;
using RR3CommunityServer.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddRazorPages(); // Add Razor Pages support
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Database
builder.Services.AddDbContext<RR3DbContext>(options =>
options.UseSqlite("Data Source=rr3community.db"));
// Custom services
builder.Services.AddScoped<ISessionService, SessionService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ICatalogService, CatalogService>();
builder.Services.AddScoped<IDrmService, DrmService>();
// CORS for cross-origin requests
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// Initialize database
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<RR3DbContext>();
db.Database.EnsureCreated();
}
app.UseHttpsRedirection();
app.UseCors();
// Custom middleware
app.UseMiddleware<SynergyHeadersMiddleware>();
app.UseMiddleware<SessionValidationMiddleware>();
app.UseAuthorization();
app.MapControllers();
app.MapRazorPages(); // Add Razor Pages routing
// Redirect root to admin panel
app.MapGet("/", () => Results.Redirect("/admin"));
Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Real Racing 3 Community Server - RUNNING ║");
Console.WriteLine("╠══════════════════════════════════════════════════════════╣");
Console.WriteLine("║ Server is ready to accept connections ║");
Console.WriteLine("║ Ensure DNS/hosts file points EA servers to this IP ║");
Console.WriteLine("╚══════════════════════════════════════════════════════════╝");
Console.WriteLine();
Console.WriteLine("Listening on: https://localhost:5001");
Console.WriteLine("Director endpoint: /director/api/android/getDirectionByPackage");
Console.WriteLine();
app.Run();

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:3254",
"sslPort": 44396
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5143",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7086;http://localhost:5143",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.24" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@RR3CommunityServer_HostAddress = http://localhost:5143
GET {{RR3CommunityServer_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,34 @@
namespace RR3CommunityServer.Services;
// Session Service
public interface ISessionService
{
Task<string> CreateSession(string? synergyId = null);
Task<bool> ValidateSession(string sessionId);
Task<string?> GetSynergyIdFromSession(string sessionId);
}
// User Service
public interface IUserService
{
Task<string> GetOrCreateDeviceId(string? existingDeviceId, string hardwareId);
Task<string> ValidateDeviceId(string deviceId);
Task<string> GetOrCreateAnonUid();
Task<string> GetOrCreateSynergyId(string deviceId);
}
// Catalog Service
public interface ICatalogService
{
Task<List<Models.CatalogItem>> GetAvailableItems();
Task<List<Models.CatalogCategory>> GetCategories();
Task<string> GetDownloadUrl(string itemId);
}
// DRM Service
public interface IDrmService
{
Task<string> GenerateNonce();
Task<List<Models.PurchasedItem>> GetPurchasedItems(string synergyId);
Task<bool> VerifyAndRecordPurchase(string synergyId, Models.PurchaseVerificationRequest request);
}

View File

@@ -0,0 +1,228 @@
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Services;
public class SessionService : ISessionService
{
private readonly RR3DbContext _context;
public SessionService(RR3DbContext context)
{
_context = context;
}
public async Task<string> CreateSession(string? synergyId = null)
{
var sessionId = Guid.NewGuid().ToString();
var session = new Session
{
SessionId = sessionId,
SynergyId = synergyId,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddHours(24)
};
_context.Sessions.Add(session);
await _context.SaveChangesAsync();
return sessionId;
}
public async Task<bool> ValidateSession(string sessionId)
{
var session = await _context.Sessions
.FirstOrDefaultAsync(s => s.SessionId == sessionId);
return session != null && session.ExpiresAt > DateTime.UtcNow;
}
public async Task<string?> GetSynergyIdFromSession(string sessionId)
{
var session = await _context.Sessions
.FirstOrDefaultAsync(s => s.SessionId == sessionId);
return session?.SynergyId;
}
}
public class UserService : IUserService
{
private readonly RR3DbContext _context;
public UserService(RR3DbContext context)
{
_context = context;
}
public async Task<string> GetOrCreateDeviceId(string? existingDeviceId, string hardwareId)
{
if (!string.IsNullOrEmpty(existingDeviceId))
{
var existing = await _context.Devices
.FirstOrDefaultAsync(d => d.DeviceId == existingDeviceId);
if (existing != null)
{
existing.LastSeenAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return existing.DeviceId;
}
}
var deviceId = Guid.NewGuid().ToString();
var device = new Device
{
DeviceId = deviceId,
HardwareId = hardwareId,
CreatedAt = DateTime.UtcNow,
LastSeenAt = DateTime.UtcNow
};
_context.Devices.Add(device);
await _context.SaveChangesAsync();
return deviceId;
}
public async Task<string> ValidateDeviceId(string deviceId)
{
var device = await _context.Devices
.FirstOrDefaultAsync(d => d.DeviceId == deviceId);
if (device != null)
{
device.LastSeenAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return "valid";
}
return "invalid";
}
public async Task<string> GetOrCreateAnonUid()
{
return await Task.FromResult(Guid.NewGuid().ToString());
}
public async Task<string> GetOrCreateSynergyId(string deviceId)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.DeviceId == deviceId);
if (user != null)
{
return user.SynergyId;
}
var synergyId = $"SYN-{Guid.NewGuid():N}";
user = new User
{
SynergyId = synergyId,
DeviceId = deviceId,
CreatedAt = DateTime.UtcNow
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
return synergyId;
}
}
public class CatalogService : ICatalogService
{
private readonly RR3DbContext _context;
public CatalogService(RR3DbContext context)
{
_context = context;
}
public async Task<List<Models.CatalogItem>> GetAvailableItems()
{
var items = await _context.CatalogItems
.Where(c => c.Available)
.ToListAsync();
return items.Select(e => new Models.CatalogItem
{
itemId = e.Sku,
sku = e.Sku,
name = e.Name,
description = e.Type,
category = e.Type,
price = e.Price,
currency = "USD"
}).ToList();
}
public async Task<List<CatalogCategory>> GetCategories()
{
var items = await _context.CatalogItems
.Where(c => c.Available)
.ToListAsync();
var categories = items.GroupBy(i => i.Type)
.Select(g => new CatalogCategory
{
categoryId = g.Key,
name = g.Key,
itemIds = g.Select(i => i.Sku).ToList()
}).ToList();
return categories;
}
public async Task<string> GetDownloadUrl(string itemId)
{
return await Task.FromResult($"https://localhost:5001/downloads/{itemId}");
}
}
public class DrmService : IDrmService
{
private readonly RR3DbContext _context;
public DrmService(RR3DbContext context)
{
_context = context;
}
public async Task<string> GenerateNonce()
{
return await Task.FromResult(Convert.ToBase64String(Guid.NewGuid().ToByteArray()));
}
public async Task<List<PurchasedItem>> GetPurchasedItems(string synergyId)
{
var purchases = await _context.Purchases
.Where(p => p.SynergyId == synergyId)
.ToListAsync();
return purchases.Select(p => new PurchasedItem
{
itemId = p.ItemId,
sku = p.Sku,
orderId = p.OrderId,
purchaseTime = new DateTimeOffset(p.PurchaseTime).ToUnixTimeSeconds(),
token = p.Token
}).ToList();
}
public async Task<bool> VerifyAndRecordPurchase(string synergyId, PurchaseVerificationRequest request)
{
// For community server, we accept all purchases
var purchase = new Purchase
{
SynergyId = synergyId,
ItemId = request.sku,
Sku = request.sku,
OrderId = request.orderId,
PurchaseTime = DateTime.UtcNow,
Token = request.receipt
};
_context.Purchases.Add(purchase);
await _context.SaveChangesAsync();
return true;
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Binary file not shown.

View File

@@ -0,0 +1,945 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"RR3CommunityServer/1.0.0": {
"dependencies": {
"Microsoft.AspNetCore.OpenApi": "8.0.24",
"Microsoft.EntityFrameworkCore": "8.0.11",
"Microsoft.EntityFrameworkCore.Design": "8.0.11",
"Microsoft.EntityFrameworkCore.Sqlite": "8.0.11",
"Swashbuckle.AspNetCore": "6.6.2"
},
"runtime": {
"RR3CommunityServer.dll": {}
}
},
"Humanizer.Core/2.14.1": {
"runtime": {
"lib/net6.0/Humanizer.dll": {
"assemblyVersion": "2.14.0.0",
"fileVersion": "2.14.1.48190"
}
}
},
"Microsoft.AspNetCore.OpenApi/8.0.24": {
"dependencies": {
"Microsoft.OpenApi": "1.6.14"
},
"runtime": {
"lib/net8.0/Microsoft.AspNetCore.OpenApi.dll": {
"assemblyVersion": "8.0.24.0",
"fileVersion": "8.0.2426.7207"
}
}
},
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
"runtime": {
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.CodeAnalysis.Common/4.5.0": {
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
"dependencies": {
"Microsoft.CodeAnalysis.Common": "4.5.0"
},
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
"dependencies": {
"Humanizer.Core": "2.14.1",
"Microsoft.CodeAnalysis.CSharp": "4.5.0",
"Microsoft.CodeAnalysis.Common": "4.5.0",
"Microsoft.CodeAnalysis.Workspaces.Common": "4.5.0"
},
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.Workspaces.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
"dependencies": {
"Humanizer.Core": "2.14.1",
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
"Microsoft.CodeAnalysis.Common": "4.5.0",
"System.Composition": "6.0.0"
},
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.Workspaces.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.Data.Sqlite.Core/8.0.11": {
"dependencies": {
"SQLitePCLRaw.core": "2.1.6"
},
"runtime": {
"lib/net8.0/Microsoft.Data.Sqlite.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52104"
}
}
},
"Microsoft.EntityFrameworkCore/8.0.11": {
"dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.11",
"Microsoft.Extensions.Caching.Memory": "8.0.1",
"Microsoft.Extensions.Logging": "8.0.1"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52104"
}
}
},
"Microsoft.EntityFrameworkCore.Abstractions/8.0.11": {
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52104"
}
}
},
"Microsoft.EntityFrameworkCore.Design/8.0.11": {
"dependencies": {
"Humanizer.Core": "2.14.1",
"Microsoft.CodeAnalysis.CSharp.Workspaces": "4.5.0",
"Microsoft.EntityFrameworkCore.Relational": "8.0.11",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Mono.TextTemplating": "2.2.1"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Design.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52104"
}
}
},
"Microsoft.EntityFrameworkCore.Relational/8.0.11": {
"dependencies": {
"Microsoft.EntityFrameworkCore": "8.0.11"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52104"
}
}
},
"Microsoft.EntityFrameworkCore.Sqlite/8.0.11": {
"dependencies": {
"Microsoft.EntityFrameworkCore.Sqlite.Core": "8.0.11",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core/8.0.11": {
"dependencies": {
"Microsoft.Data.Sqlite.Core": "8.0.11",
"Microsoft.EntityFrameworkCore.Relational": "8.0.11",
"Microsoft.Extensions.DependencyModel": "8.0.2"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Sqlite.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52104"
}
}
},
"Microsoft.Extensions.Caching.Memory/8.0.1": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
},
"runtime": {
"lib/net8.0/Microsoft.Extensions.Caching.Memory.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.0.1024.46610"
}
}
},
"Microsoft.Extensions.DependencyInjection/8.0.1": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
},
"runtime": {
"lib/net8.0/Microsoft.Extensions.DependencyInjection.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.0.1024.46610"
}
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
"runtime": {
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.0.1024.46610"
}
}
},
"Microsoft.Extensions.DependencyModel/8.0.2": {
"runtime": {
"lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
"assemblyVersion": "8.0.0.2",
"fileVersion": "8.0.1024.46610"
}
}
},
"Microsoft.Extensions.Logging/8.0.1": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
},
"runtime": {
"lib/net8.0/Microsoft.Extensions.Logging.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.0.1024.46610"
}
}
},
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
},
"runtime": {
"lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.0.1024.46610"
}
}
},
"Microsoft.Extensions.Options/8.0.2": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
},
"runtime": {
"lib/net8.0/Microsoft.Extensions.Options.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.0.224.6711"
}
}
},
"Microsoft.OpenApi/1.6.14": {
"runtime": {
"lib/netstandard2.0/Microsoft.OpenApi.dll": {
"assemblyVersion": "1.6.14.0",
"fileVersion": "1.6.14.0"
}
}
},
"Mono.TextTemplating/2.2.1": {
"dependencies": {
"System.CodeDom": "4.4.0"
},
"runtime": {
"lib/netstandard2.0/Mono.TextTemplating.dll": {
"assemblyVersion": "2.2.0.0",
"fileVersion": "2.2.1.1"
}
}
},
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
"dependencies": {
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6",
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6"
},
"runtime": {
"lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": {
"assemblyVersion": "2.1.6.2060",
"fileVersion": "2.1.6.2060"
}
}
},
"SQLitePCLRaw.core/2.1.6": {
"runtime": {
"lib/netstandard2.0/SQLitePCLRaw.core.dll": {
"assemblyVersion": "2.1.6.2060",
"fileVersion": "2.1.6.2060"
}
}
},
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
"runtimeTargets": {
"runtimes/browser-wasm/nativeassets/net8.0/e_sqlite3.a": {
"rid": "browser-wasm",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-arm/native/libe_sqlite3.so": {
"rid": "linux-arm",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-arm64/native/libe_sqlite3.so": {
"rid": "linux-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-armel/native/libe_sqlite3.so": {
"rid": "linux-armel",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-mips64/native/libe_sqlite3.so": {
"rid": "linux-mips64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-arm/native/libe_sqlite3.so": {
"rid": "linux-musl-arm",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-arm64/native/libe_sqlite3.so": {
"rid": "linux-musl-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-x64/native/libe_sqlite3.so": {
"rid": "linux-musl-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-ppc64le/native/libe_sqlite3.so": {
"rid": "linux-ppc64le",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-s390x/native/libe_sqlite3.so": {
"rid": "linux-s390x",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-x64/native/libe_sqlite3.so": {
"rid": "linux-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-x86/native/libe_sqlite3.so": {
"rid": "linux-x86",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/maccatalyst-arm64/native/libe_sqlite3.dylib": {
"rid": "maccatalyst-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/maccatalyst-x64/native/libe_sqlite3.dylib": {
"rid": "maccatalyst-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/osx-arm64/native/libe_sqlite3.dylib": {
"rid": "osx-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/osx-x64/native/libe_sqlite3.dylib": {
"rid": "osx-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-arm/native/e_sqlite3.dll": {
"rid": "win-arm",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-arm64/native/e_sqlite3.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x64/native/e_sqlite3.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x86/native/e_sqlite3.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
"dependencies": {
"SQLitePCLRaw.core": "2.1.6"
},
"runtime": {
"lib/net6.0/SQLitePCLRaw.provider.e_sqlite3.dll": {
"assemblyVersion": "2.1.6.2060",
"fileVersion": "2.1.6.2060"
}
}
},
"Swashbuckle.AspNetCore/6.6.2": {
"dependencies": {
"Swashbuckle.AspNetCore.Swagger": "6.6.2",
"Swashbuckle.AspNetCore.SwaggerGen": "6.6.2",
"Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
}
},
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
"dependencies": {
"Microsoft.OpenApi": "1.6.14"
},
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
"assemblyVersion": "6.6.2.0",
"fileVersion": "6.6.2.401"
}
}
},
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
"dependencies": {
"Swashbuckle.AspNetCore.Swagger": "6.6.2"
},
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
"assemblyVersion": "6.6.2.0",
"fileVersion": "6.6.2.401"
}
}
},
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
"assemblyVersion": "6.6.2.0",
"fileVersion": "6.6.2.401"
}
}
},
"System.CodeDom/4.4.0": {
"runtime": {
"lib/netstandard2.0/System.CodeDom.dll": {
"assemblyVersion": "4.0.0.0",
"fileVersion": "4.6.25519.3"
}
}
},
"System.Composition/6.0.0": {
"dependencies": {
"System.Composition.AttributedModel": "6.0.0",
"System.Composition.Convention": "6.0.0",
"System.Composition.Hosting": "6.0.0",
"System.Composition.Runtime": "6.0.0",
"System.Composition.TypedParts": "6.0.0"
}
},
"System.Composition.AttributedModel/6.0.0": {
"runtime": {
"lib/net6.0/System.Composition.AttributedModel.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.Convention/6.0.0": {
"dependencies": {
"System.Composition.AttributedModel": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Composition.Convention.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.Hosting/6.0.0": {
"dependencies": {
"System.Composition.Runtime": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Composition.Hosting.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.Runtime/6.0.0": {
"runtime": {
"lib/net6.0/System.Composition.Runtime.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.TypedParts/6.0.0": {
"dependencies": {
"System.Composition.AttributedModel": "6.0.0",
"System.Composition.Hosting": "6.0.0",
"System.Composition.Runtime": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Composition.TypedParts.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
}
}
},
"libraries": {
"RR3CommunityServer/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Humanizer.Core/2.14.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==",
"path": "humanizer.core/2.14.1",
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
},
"Microsoft.AspNetCore.OpenApi/8.0.24": {
"type": "package",
"serviceable": true,
"sha512": "sha512-rqHY6POxy1e0vf7opG5hsxR0+Z0svcMYDvaEQW+T93/YeyFlaFOqQkZ6t1C8SaNLyH6LFlSnOXQ1Jf9Q+JFEhg==",
"path": "microsoft.aspnetcore.openapi/8.0.24",
"hashPath": "microsoft.aspnetcore.openapi.8.0.24.nupkg.sha512"
},
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==",
"path": "microsoft.bcl.asyncinterfaces/6.0.0",
"hashPath": "microsoft.bcl.asyncinterfaces.6.0.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.Common/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lwAbIZNdnY0SUNoDmZHkVUwLO8UyNnyyh1t/4XsbFxi4Ounb3xszIYZaWhyj5ZjyfcwqwmtMbE7fUTVCqQEIdQ==",
"path": "microsoft.codeanalysis.common/4.5.0",
"hashPath": "microsoft.codeanalysis.common.4.5.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-cM59oMKAOxvdv76bdmaKPy5hfj+oR+zxikWoueEB7CwTko7mt9sVKZI8Qxlov0C/LuKEG+WQwifepqL3vuTiBQ==",
"path": "microsoft.codeanalysis.csharp/4.5.0",
"hashPath": "microsoft.codeanalysis.csharp.4.5.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-h74wTpmGOp4yS4hj+EvNzEiPgg/KVs2wmSfTZ81upJZOtPkJsVkgfsgtxxqmAeapjT/vLKfmYV0bS8n5MNVP+g==",
"path": "microsoft.codeanalysis.csharp.workspaces/4.5.0",
"hashPath": "microsoft.codeanalysis.csharp.workspaces.4.5.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-l4dDRmGELXG72XZaonnOeORyD/T5RpEu5LGHOUIhnv+MmUWDY/m1kWXGwtcgQ5CJ5ynkFiRnIYzTKXYjUs7rbw==",
"path": "microsoft.codeanalysis.workspaces.common/4.5.0",
"hashPath": "microsoft.codeanalysis.workspaces.common.4.5.0.nupkg.sha512"
},
"Microsoft.Data.Sqlite.Core/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-PrDkI9SeU/MEP/IHriczeYmRVbzEcfp66UlZRjL5ikHIJGIYOrby55GoehLCJzJiTwJ+rGkjSRctZnWgfC95fg==",
"path": "microsoft.data.sqlite.core/8.0.11",
"hashPath": "microsoft.data.sqlite.core.8.0.11.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-stbjWBTtpQ1HtqXMFyKnXFTr76PvaOHI2b2h85JqBi3eZr00nspvR/a90Zwh8CQ4rVawqLiTG0+0yZQWaav+sQ==",
"path": "microsoft.entityframeworkcore/8.0.11",
"hashPath": "microsoft.entityframeworkcore.8.0.11.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Abstractions/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-++zY0Ea724ku1jptWJmF7jm3I4IXTexfT4qi1ETcSFFF7qj+qm6rRgN7mTuKkwIETuXk0ikfzudryRjUGrrNKQ==",
"path": "microsoft.entityframeworkcore.abstractions/8.0.11",
"hashPath": "microsoft.entityframeworkcore.abstractions.8.0.11.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Design/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KxOvpbaKiUmbLvenr0T/4F1Vdm0Sq+iajLbesQK7/WKB/Dx+FQHCZ0f5jCXrVWK2QKF9eHzQ5JPA1L6hcb25FQ==",
"path": "microsoft.entityframeworkcore.design/8.0.11",
"hashPath": "microsoft.entityframeworkcore.design.8.0.11.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Relational/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3TuuW3i5I4Ro0yoaHmi2MqEDGObOVuhLaMEnd/heaLB1fcvm4fu4PevmC4BOWnI0vo176AIlV5o4rEQciLoohw==",
"path": "microsoft.entityframeworkcore.relational/8.0.11",
"hashPath": "microsoft.entityframeworkcore.relational.8.0.11.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Sqlite/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HJN+xx8lomTIq7SpshnUzHt7uo1/AOvnPWjXsOzyCsoYMEpfRKjxsJobcHu8Qpvd2mwzZB/mzjPUE8XeuGiCGA==",
"path": "microsoft.entityframeworkcore.sqlite/8.0.11",
"hashPath": "microsoft.entityframeworkcore.sqlite.8.0.11.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Sqlite.Core/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-wvC/xpis//IG9qvfMbMFMjhrM+P7choZ23CHBRfQyfmIkOVZLBtzM6nestbDdAv3eGnJym1/m0o0sc7YXlL0yg==",
"path": "microsoft.entityframeworkcore.sqlite.core/8.0.11",
"hashPath": "microsoft.entityframeworkcore.sqlite.core.8.0.11.nupkg.sha512"
},
"Microsoft.Extensions.Caching.Memory/8.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HFDnhYLccngrzyGgHkjEDU5FMLn4MpOsr5ElgsBMC4yx6lJh4jeWO7fHS8+TXPq+dgxCmUa/Trl8svObmwW4QA==",
"path": "microsoft.extensions.caching.memory/8.0.1",
"hashPath": "microsoft.extensions.caching.memory.8.0.1.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection/8.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
"path": "microsoft.extensions.dependencyinjection/8.0.1",
"hashPath": "microsoft.extensions.dependencyinjection.8.0.1.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==",
"path": "microsoft.extensions.dependencyinjection.abstractions/8.0.2",
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.2.nupkg.sha512"
},
"Microsoft.Extensions.DependencyModel/8.0.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==",
"path": "microsoft.extensions.dependencymodel/8.0.2",
"hashPath": "microsoft.extensions.dependencymodel.8.0.2.nupkg.sha512"
},
"Microsoft.Extensions.Logging/8.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
"path": "microsoft.extensions.logging/8.0.1",
"hashPath": "microsoft.extensions.logging.8.0.1.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
"path": "microsoft.extensions.logging.abstractions/8.0.2",
"hashPath": "microsoft.extensions.logging.abstractions.8.0.2.nupkg.sha512"
},
"Microsoft.Extensions.Options/8.0.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"path": "microsoft.extensions.options/8.0.2",
"hashPath": "microsoft.extensions.options.8.0.2.nupkg.sha512"
},
"Microsoft.OpenApi/1.6.14": {
"type": "package",
"serviceable": true,
"sha512": "sha512-tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==",
"path": "microsoft.openapi/1.6.14",
"hashPath": "microsoft.openapi.1.6.14.nupkg.sha512"
},
"Mono.TextTemplating/2.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==",
"path": "mono.texttemplating/2.2.1",
"hashPath": "mono.texttemplating.2.2.1.nupkg.sha512"
},
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==",
"path": "sqlitepclraw.bundle_e_sqlite3/2.1.6",
"hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.6.nupkg.sha512"
},
"SQLitePCLRaw.core/2.1.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==",
"path": "sqlitepclraw.core/2.1.6",
"hashPath": "sqlitepclraw.core.2.1.6.nupkg.sha512"
},
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==",
"path": "sqlitepclraw.lib.e_sqlite3/2.1.6",
"hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.6.nupkg.sha512"
},
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==",
"path": "sqlitepclraw.provider.e_sqlite3/2.1.6",
"hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.6.nupkg.sha512"
},
"Swashbuckle.AspNetCore/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-+NB4UYVYN6AhDSjW0IJAd1AGD8V33gemFNLPaxKTtPkHB+HaKAKf9MGAEUPivEWvqeQfcKIw8lJaHq6LHljRuw==",
"path": "swashbuckle.aspnetcore/6.6.2",
"hashPath": "swashbuckle.aspnetcore.6.6.2.nupkg.sha512"
},
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ovgPTSYX83UrQUWiS5vzDcJ8TEX1MAxBgDFMK45rC24MorHEPQlZAHlaXj/yth4Zf6xcktpUgTEBvffRQVwDKA==",
"path": "swashbuckle.aspnetcore.swagger/6.6.2",
"hashPath": "swashbuckle.aspnetcore.swagger.6.6.2.nupkg.sha512"
},
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zv4ikn4AT1VYuOsDCpktLq4QDq08e7Utzbir86M5/ZkRaLXbCPF11E1/vTmOiDzRTl0zTZINQU2qLKwTcHgfrA==",
"path": "swashbuckle.aspnetcore.swaggergen/6.6.2",
"hashPath": "swashbuckle.aspnetcore.swaggergen.6.6.2.nupkg.sha512"
},
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==",
"path": "swashbuckle.aspnetcore.swaggerui/6.6.2",
"hashPath": "swashbuckle.aspnetcore.swaggerui.6.6.2.nupkg.sha512"
},
"System.CodeDom/4.4.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2sCCb7doXEwtYAbqzbF/8UAeDRMNmPaQbU2q50Psg1J9KzumyVVCgKQY8s53WIPTufNT0DpSe9QRvVjOzfDWBA==",
"path": "system.codedom/4.4.0",
"hashPath": "system.codedom.4.4.0.nupkg.sha512"
},
"System.Composition/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-d7wMuKQtfsxUa7S13tITC8n1cQzewuhD5iDjZtK2prwFfKVzdYtgrTHgjaV03Zq7feGQ5gkP85tJJntXwInsJA==",
"path": "system.composition/6.0.0",
"hashPath": "system.composition.6.0.0.nupkg.sha512"
},
"System.Composition.AttributedModel/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WK1nSDLByK/4VoC7fkNiFuTVEiperuCN/Hyn+VN30R+W2ijO1d0Z2Qm0ScEl9xkSn1G2MyapJi8xpf4R8WRa/w==",
"path": "system.composition.attributedmodel/6.0.0",
"hashPath": "system.composition.attributedmodel.6.0.0.nupkg.sha512"
},
"System.Composition.Convention/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-XYi4lPRdu5bM4JVJ3/UIHAiG6V6lWWUlkhB9ab4IOq0FrRsp0F4wTyV4Dj+Ds+efoXJ3qbLqlvaUozDO7OLeXA==",
"path": "system.composition.convention/6.0.0",
"hashPath": "system.composition.convention.6.0.0.nupkg.sha512"
},
"System.Composition.Hosting/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-w/wXjj7kvxuHPLdzZ0PAUt++qJl03t7lENmb2Oev0n3zbxyNULbWBlnd5J5WUMMv15kg5o+/TCZFb6lSwfaUUQ==",
"path": "system.composition.hosting/6.0.0",
"hashPath": "system.composition.hosting.6.0.0.nupkg.sha512"
},
"System.Composition.Runtime/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-qkRH/YBaMPTnzxrS5RDk1juvqed4A6HOD/CwRcDGyPpYps1J27waBddiiq1y93jk2ZZ9wuA/kynM+NO0kb3PKg==",
"path": "system.composition.runtime/6.0.0",
"hashPath": "system.composition.runtime.6.0.0.nupkg.sha512"
},
"System.Composition.TypedParts/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iUR1eHrL8Cwd82neQCJ00MpwNIBs4NZgXzrPqx8NJf/k4+mwBO0XCRmHYJT4OLSwDDqh5nBLJWkz5cROnrGhRA==",
"path": "system.composition.typedparts/6.0.0",
"hashPath": "system.composition.typedparts.6.0.0.nupkg.sha512"
}
}
}

View File

@@ -0,0 +1,20 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "8.0.0"
}
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

View File

@@ -0,0 +1 @@
{"Version":1,"ManifestType":"Build","Endpoints":[]}

View File

@@ -0,0 +1 @@
{"ContentRoots":["E:\\rr3\\RR3CommunityServer\\RR3CommunityServer\\wwwroot\\"],"Root":{"Children":null,"Asset":null,"Patterns":[{"ContentRootIndex":0,"Pattern":"**","Depth":0}]}}

Binary file not shown.

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Some files were not shown because too many files have changed in this diff Show More