Compare commits

...

5 Commits

Author SHA1 Message Date
15e842ce85 Fix CSRF token issue in login and register forms
- Added @Html.AntiForgeryToken() to Register.cshtml
- Added @Html.AntiForgeryToken() to Login.cshtml
- Fixes 400 Bad Request errors on form submission

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-19 16:03:42 -08:00
f1d0d43cb7 Add asset management migration
- UpdateGameAssetFields migration for new columns
- Added Description, IsRequired, UploadedAt to GameAssets
- Fixed nullable LocalPath and FileSha256

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-19 15:39:28 -08:00
5d2c3bf880 Add asset management system
- Created Assets.cshtml and Assets.cshtml.cs for admin panel
- Upload assets with MD5/SHA256 hash calculation
- Generate asset manifests in RR3 format (tab-separated)
- Integrated with Nimble SDK asset download system
- Updated GameAsset model with IsRequired, UploadedAt, Description
- Added navigation link in _Layout.cshtml
- Supports categories: base, cars, tracks, audio, textures, UI, DLC
- Asset download endpoint at /content/api/{assetPath}
- Manifest endpoint at /content/api/manifest

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-19 15:16:43 -08:00
e03c1d9856 Add admin panel authentication and login system
Features:
- Login page with username/email + password
- Registration page for new accounts
- Logout functionality
- Cookie-based authentication (30-day sessions)
- Auto-redirect to login for unauthorized access
- User dropdown in navbar with logout link

Security:
- All admin pages now require authentication
- [Authorize] attribute on all admin PageModels
- Redirect to /Login if not authenticated
- Auto-login after registration

UI:
- Beautiful gradient login/register pages
- Consistent styling with admin panel
- User info displayed in navbar
- Logout link in dropdown menu

Starting resources for new users:
- 100,000 Gold
- 500,000 Cash
- Level 1
- Full admin panel access

Ready for production deployment!
2026-02-19 15:06:08 -08:00
a6bab92282 Add user authentication and account management system
Features:
- User registration with username, email, password
- Login with JWT token authentication
- Password hashing with BCrypt
- Account settings & management
- Device linking to accounts
- Change password & password reset
- Account-User relationship (1-to-1 with game data)

Database entities:
- Account: User accounts with credentials
- DeviceAccount: Link devices to accounts (many-to-many)

API endpoints:
- POST /api/auth/register
- POST /api/auth/login
- POST /api/auth/change-password
- POST /api/auth/forgot-password
- POST /api/auth/reset-password
- GET /api/auth/me
- POST /api/auth/link-device
- DELETE /api/auth/unlink-device/{deviceId}

Starting resources for new accounts:
- 100,000 Gold
- 500,000 Cash
- Level 1

Ready for VPS deployment with HTTPS.
2026-02-19 15:00:16 -08:00
64 changed files with 3789 additions and 22 deletions

View File

@@ -0,0 +1,128 @@
using Microsoft.AspNetCore.Mvc;
using RR3CommunityServer.Models;
using RR3CommunityServer.Services;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
private readonly ILogger<AuthController> _logger;
public AuthController(IAuthService authService, ILogger<AuthController> logger)
{
_authService = authService;
_logger = logger;
}
[HttpPost("register")]
public async Task<ActionResult> Register([FromBody] RegisterRequest request)
{
var (success, token, error) = await _authService.RegisterAsync(request);
if (!success)
return BadRequest(new { message = error });
return Ok(new { message = "Account created successfully", token });
}
[HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest request)
{
var (success, response, error) = await _authService.LoginAsync(request);
if (!success)
return Unauthorized(new { message = error });
return Ok(response);
}
[HttpPost("change-password")]
public async Task<ActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var account = await _authService.ValidateTokenAsync(token);
if (account == null)
return Unauthorized(new { message = "Invalid or expired token" });
var (success, error) = await _authService.ChangePasswordAsync(account.Id, request);
if (!success)
return BadRequest(new { message = error });
return Ok(new { message = "Password changed successfully" });
}
[HttpPost("forgot-password")]
public async Task<ActionResult> ForgotPassword([FromBody] ForgotPasswordRequest request)
{
var (success, error) = await _authService.ForgotPasswordAsync(request);
if (!success)
return BadRequest(new { message = error });
return Ok(new { message = "Password reset instructions sent to your email" });
}
[HttpPost("reset-password")]
public async Task<ActionResult> ResetPassword([FromBody] ResetPasswordRequest request)
{
var (success, error) = await _authService.ResetPasswordAsync(request);
if (!success)
return BadRequest(new { message = error });
return Ok(new { message = "Password reset successfully" });
}
[HttpGet("me")]
public async Task<ActionResult> GetCurrentUser()
{
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var account = await _authService.ValidateTokenAsync(token);
if (account == null)
return Unauthorized(new { message = "Invalid or expired token" });
var settings = await _authService.GetAccountSettingsAsync(account.Id);
return Ok(settings);
}
[HttpPost("link-device")]
public async Task<ActionResult> LinkDevice([FromBody] LinkDeviceRequest request)
{
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var account = await _authService.ValidateTokenAsync(token);
if (account == null)
return Unauthorized(new { message = "Invalid or expired token" });
var (success, error) = await _authService.LinkDeviceAsync(account.Id, request);
if (!success)
return BadRequest(new { message = error });
return Ok(new { message = "Device linked successfully" });
}
[HttpDelete("unlink-device/{deviceId}")]
public async Task<ActionResult> UnlinkDevice(string deviceId)
{
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var account = await _authService.ValidateTokenAsync(token);
if (account == null)
return Unauthorized(new { message = "Invalid or expired token" });
var (success, error) = await _authService.UnlinkDeviceAsync(account.Id, deviceId);
if (!success)
return BadRequest(new { message = error });
return Ok(new { message = "Device unlinked successfully" });
}
}

View File

@@ -10,6 +10,8 @@ public class RR3DbContext : DbContext
public DbSet<Device> Devices { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Session> Sessions { get; set; }
public DbSet<Account> Accounts { get; set; }
public DbSet<DeviceAccount> DeviceAccounts { get; set; }
public DbSet<Purchase> Purchases { get; set; }
public DbSet<CatalogItem> CatalogItems { get; set; }
public DbSet<DailyReward> DailyRewards { get; set; }
@@ -379,16 +381,18 @@ public class GameAsset
public string? EaCdnPath { get; set; }
// Local storage
public string LocalPath { get; set; } = string.Empty;
public string? LocalPath { get; set; }
public long FileSize { get; set; }
public string FileSha256 { get; set; } = string.Empty;
public string? FileSha256 { get; set; }
public string? Version { get; set; }
// Metadata
public DateTime DownloadedAt { get; set; } = DateTime.UtcNow;
public DateTime UploadedAt { get; set; } = DateTime.UtcNow;
public DateTime LastAccessedAt { get; set; } = DateTime.UtcNow;
public int AccessCount { get; set; } = 0;
public bool IsAvailable { get; set; } = true;
public bool IsRequired { get; set; } = false;
// Game-specific (optional)
public string? CarId { get; set; }
@@ -396,6 +400,7 @@ public class GameAsset
public string Category { get; set; } = "misc"; // models, textures, audio, etc.
public long? CompressedSize { get; set; }
public string? Md5Hash { get; set; }
public string? Description { get; set; }
// Custom content support
public bool IsCustomContent { get; set; }

View File

@@ -0,0 +1,966 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RR3CommunityServer.Data;
#nullable disable
namespace RR3CommunityServer.Migrations
{
[DbContext(typeof(RR3DbContext))]
[Migration("20260219233025_UpdateGameAssetFields")]
partial class UpdateGameAssetFields
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("RR3CommunityServer.Data.Car", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Available")
.HasColumnType("INTEGER");
b.Property<int>("BasePerformanceRating")
.HasColumnType("INTEGER");
b.Property<string>("CarId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("CashPrice")
.HasColumnType("INTEGER");
b.Property<string>("ClassType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CustomAuthor")
.HasColumnType("TEXT");
b.Property<string>("CustomVersion")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<int>("GoldPrice")
.HasColumnType("INTEGER");
b.Property<bool>("IsCustom")
.HasColumnType("INTEGER");
b.Property<string>("Manufacturer")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Year")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Cars");
b.HasData(
new
{
Id = 1,
Available = true,
BasePerformanceRating = 45,
CarId = "nissan_silvia_s15",
CashPrice = 25000,
ClassType = "C",
GoldPrice = 0,
IsCustom = false,
Manufacturer = "Nissan",
Name = "Nissan Silvia Spec-R",
Year = 0
},
new
{
Id = 2,
Available = true,
BasePerformanceRating = 58,
CarId = "ford_focus_rs",
CashPrice = 85000,
ClassType = "B",
GoldPrice = 150,
IsCustom = false,
Manufacturer = "Ford",
Name = "Ford Focus RS",
Year = 0
},
new
{
Id = 3,
Available = true,
BasePerformanceRating = 72,
CarId = "porsche_911_gt3",
CashPrice = 0,
ClassType = "A",
GoldPrice = 350,
IsCustom = false,
Manufacturer = "Porsche",
Name = "Porsche 911 GT3 RS",
Year = 0
},
new
{
Id = 4,
Available = true,
BasePerformanceRating = 88,
CarId = "ferrari_488_gtb",
CashPrice = 0,
ClassType = "S",
GoldPrice = 750,
IsCustom = false,
Manufacturer = "Ferrari",
Name = "Ferrari 488 GTB",
Year = 0
},
new
{
Id = 5,
Available = true,
BasePerformanceRating = 105,
CarId = "mclaren_p1_gtr",
CashPrice = 0,
ClassType = "R",
GoldPrice = 1500,
IsCustom = false,
Manufacturer = "McLaren",
Name = "McLaren P1 GTR",
Year = 0
});
});
modelBuilder.Entity("RR3CommunityServer.Data.CarUpgrade", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CarId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("CashCost")
.HasColumnType("INTEGER");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<int>("PerformanceIncrease")
.HasColumnType("INTEGER");
b.Property<string>("UpgradeType")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("CarUpgrades");
b.HasData(
new
{
Id = 1,
CarId = "nissan_silvia_s15",
CashCost = 5000,
Level = 1,
PerformanceIncrease = 3,
UpgradeType = "engine"
},
new
{
Id = 2,
CarId = "nissan_silvia_s15",
CashCost = 3000,
Level = 1,
PerformanceIncrease = 2,
UpgradeType = "tires"
},
new
{
Id = 3,
CarId = "nissan_silvia_s15",
CashCost = 4000,
Level = 1,
PerformanceIncrease = 2,
UpgradeType = "suspension"
},
new
{
Id = 4,
CarId = "nissan_silvia_s15",
CashCost = 3500,
Level = 1,
PerformanceIncrease = 2,
UpgradeType = "brakes"
},
new
{
Id = 5,
CarId = "nissan_silvia_s15",
CashCost = 4500,
Level = 1,
PerformanceIncrease = 3,
UpgradeType = "drivetrain"
});
});
modelBuilder.Entity("RR3CommunityServer.Data.CareerProgress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<double>("BestTime")
.HasColumnType("REAL");
b.Property<bool>("Completed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("TEXT");
b.Property<string>("EventName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SeriesName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("StarsEarned")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("CareerProgress");
});
modelBuilder.Entity("RR3CommunityServer.Data.CatalogItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Available")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("Price")
.HasColumnType("TEXT");
b.Property<string>("Sku")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("CatalogItems");
b.HasData(
new
{
Id = 1,
Available = true,
Name = "1000 Gold",
Price = 0.99m,
Sku = "com.ea.rr3.gold_1000",
Type = "currency"
},
new
{
Id = 2,
Available = true,
Name = "Starter Car",
Price = 0m,
Sku = "com.ea.rr3.car_tier1",
Type = "car"
},
new
{
Id = 3,
Available = true,
Name = "Engine Upgrade",
Price = 4.99m,
Sku = "com.ea.rr3.upgrade_engine",
Type = "upgrade"
},
new
{
Id = 4,
Available = true,
Name = "100 Gold",
Price = 0m,
Sku = "com.ea.rr3.gold_100",
Type = "currency"
},
new
{
Id = 5,
Available = true,
Name = "500 Gold",
Price = 0m,
Sku = "com.ea.rr3.gold_500",
Type = "currency"
},
new
{
Id = 6,
Available = true,
Name = "1000 Gold",
Price = 0m,
Sku = "com.ea.rr3.gold_1000",
Type = "currency"
},
new
{
Id = 7,
Available = true,
Name = "5000 Gold",
Price = 0m,
Sku = "com.ea.rr3.gold_5000",
Type = "currency"
});
});
modelBuilder.Entity("RR3CommunityServer.Data.DailyReward", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CashAmount")
.HasColumnType("INTEGER");
b.Property<bool>("Claimed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ClaimedAt")
.HasColumnType("TEXT");
b.Property<int>("GoldAmount")
.HasColumnType("INTEGER");
b.Property<DateTime>("RewardDate")
.HasColumnType("TEXT");
b.Property<int>("Streak")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("DailyRewards");
});
modelBuilder.Entity("RR3CommunityServer.Data.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HardwareId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Devices");
});
modelBuilder.Entity("RR3CommunityServer.Data.GameAsset", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccessCount")
.HasColumnType("INTEGER");
b.Property<string>("AssetId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AssetType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CarId")
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long?>("CompressedSize")
.HasColumnType("INTEGER");
b.Property<string>("ContentType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CustomAuthor")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<DateTime>("DownloadedAt")
.HasColumnType("TEXT");
b.Property<string>("EaCdnPath")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FileSha256")
.HasColumnType("TEXT");
b.Property<long>("FileSize")
.HasColumnType("INTEGER");
b.Property<bool>("IsAvailable")
.HasColumnType("INTEGER");
b.Property<bool>("IsCustomContent")
.HasColumnType("INTEGER");
b.Property<bool>("IsRequired")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastAccessedAt")
.HasColumnType("TEXT");
b.Property<string>("LocalPath")
.HasColumnType("TEXT");
b.Property<string>("Md5Hash")
.HasColumnType("TEXT");
b.Property<string>("OriginalUrl")
.HasColumnType("TEXT");
b.Property<string>("TrackId")
.HasColumnType("TEXT");
b.Property<DateTime>("UploadedAt")
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("GameAssets");
});
modelBuilder.Entity("RR3CommunityServer.Data.ModPack", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CarIds")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<int>("DownloadCount")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PackId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Rating")
.HasColumnType("REAL");
b.Property<string>("TrackIds")
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Version")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ModPacks");
});
modelBuilder.Entity("RR3CommunityServer.Data.OwnedCar", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CarId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CarName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ClassType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Manufacturer")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("PerformanceRating")
.HasColumnType("INTEGER");
b.Property<DateTime>("PurchasedAt")
.HasColumnType("TEXT");
b.Property<string>("PurchasedUpgrades")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("UpgradeLevel")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("OwnedCars");
});
modelBuilder.Entity("RR3CommunityServer.Data.Purchase", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ItemId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("OrderId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("Price")
.HasColumnType("TEXT");
b.Property<DateTime>("PurchaseTime")
.HasColumnType("TEXT");
b.Property<string>("Sku")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SynergyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Purchases");
});
modelBuilder.Entity("RR3CommunityServer.Data.Session", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SynergyId")
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Sessions");
});
modelBuilder.Entity("RR3CommunityServer.Data.TimeTrial", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<string>("CarName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("CashReward")
.HasColumnType("INTEGER");
b.Property<DateTime>("EndDate")
.HasColumnType("TEXT");
b.Property<int>("GoldReward")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("StartDate")
.HasColumnType("TEXT");
b.Property<double>("TargetTime")
.HasColumnType("REAL");
b.Property<string>("TrackName")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("TimeTrials");
b.HasData(
new
{
Id = 1,
Active = true,
CarName = "Any Car",
CashReward = 10000,
EndDate = new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2221),
GoldReward = 50,
Name = "Daily Sprint Challenge",
StartDate = new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2218),
TargetTime = 90.5,
TrackName = "Silverstone National"
},
new
{
Id = 2,
Active = true,
CarName = "Any Car",
CashReward = 25000,
EndDate = new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228),
GoldReward = 100,
Name = "Speed Demon Trial",
StartDate = new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228),
TargetTime = 120.0,
TrackName = "Dubai Autodrome"
});
});
modelBuilder.Entity("RR3CommunityServer.Data.TimeTrialResult", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("BeatTarget")
.HasColumnType("INTEGER");
b.Property<int>("CashEarned")
.HasColumnType("INTEGER");
b.Property<int>("GoldEarned")
.HasColumnType("INTEGER");
b.Property<DateTime>("SubmittedAt")
.HasColumnType("TEXT");
b.Property<double>("TimeSeconds")
.HasColumnType("REAL");
b.Property<int>("TimeTrialId")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("TimeTrialResults");
});
modelBuilder.Entity("RR3CommunityServer.Data.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("Cash")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.HasColumnType("TEXT");
b.Property<int?>("Experience")
.HasColumnType("INTEGER");
b.Property<int?>("Gold")
.HasColumnType("INTEGER");
b.Property<int?>("Level")
.HasColumnType("INTEGER");
b.Property<string>("Nickname")
.HasColumnType("TEXT");
b.Property<int?>("Reputation")
.HasColumnType("INTEGER");
b.Property<string>("SynergyId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EmailVerificationToken")
.HasColumnType("TEXT");
b.Property<bool>("EmailVerified")
.HasColumnType("INTEGER");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("PasswordResetExpiry")
.HasColumnType("TEXT");
b.Property<string>("PasswordResetToken")
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Accounts");
});
modelBuilder.Entity("RR3CommunityServer.Models.DeviceAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccountId")
.HasColumnType("INTEGER");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.HasColumnType("TEXT");
b.Property<DateTime>("LastUsedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("LinkedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("DeviceAccounts");
});
modelBuilder.Entity("RR3CommunityServer.Models.UserSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("LastUpdated")
.HasColumnType("TEXT");
b.Property<string>("Mode")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ServerUrl")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("UserSettings");
});
modelBuilder.Entity("RR3CommunityServer.Data.CareerProgress", b =>
{
b.HasOne("RR3CommunityServer.Data.User", null)
.WithMany("CareerProgress")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("RR3CommunityServer.Data.OwnedCar", b =>
{
b.HasOne("RR3CommunityServer.Data.User", null)
.WithMany("OwnedCars")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Models.DeviceAccount", b =>
{
b.HasOne("RR3CommunityServer.Models.Account", "Account")
.WithMany("LinkedDevices")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Account");
});
modelBuilder.Entity("RR3CommunityServer.Data.User", b =>
{
b.Navigation("CareerProgress");
b.Navigation("OwnedCars");
});
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
{
b.Navigation("LinkedDevices");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,182 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RR3CommunityServer.Migrations
{
/// <inheritdoc />
public partial class UpdateGameAssetFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "LocalPath",
table: "GameAssets",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "FileSha256",
table: "GameAssets",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AddColumn<string>(
name: "Description",
table: "GameAssets",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsRequired",
table: "GameAssets",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "UploadedAt",
table: "GameAssets",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.CreateTable(
name: "Accounts",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Username = table.Column<string>(type: "TEXT", nullable: false),
Email = table.Column<string>(type: "TEXT", nullable: false),
PasswordHash = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
LastLoginAt = table.Column<DateTime>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
EmailVerified = table.Column<bool>(type: "INTEGER", nullable: false),
EmailVerificationToken = table.Column<string>(type: "TEXT", nullable: true),
PasswordResetToken = table.Column<string>(type: "TEXT", nullable: true),
PasswordResetExpiry = table.Column<DateTime>(type: "TEXT", nullable: true),
UserId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Accounts", x => x.Id);
table.ForeignKey(
name: "FK_Accounts_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "DeviceAccounts",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AccountId = table.Column<int>(type: "INTEGER", nullable: false),
DeviceId = table.Column<string>(type: "TEXT", nullable: false),
DeviceName = table.Column<string>(type: "TEXT", nullable: true),
LinkedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
LastUsedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeviceAccounts", x => x.Id);
table.ForeignKey(
name: "FK_DeviceAccounts_Accounts_AccountId",
column: x => x.AccountId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2221), new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2218) });
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228), new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228) });
migrationBuilder.CreateIndex(
name: "IX_Accounts_UserId",
table: "Accounts",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_DeviceAccounts_AccountId",
table: "DeviceAccounts",
column: "AccountId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeviceAccounts");
migrationBuilder.DropTable(
name: "Accounts");
migrationBuilder.DropColumn(
name: "Description",
table: "GameAssets");
migrationBuilder.DropColumn(
name: "IsRequired",
table: "GameAssets");
migrationBuilder.DropColumn(
name: "UploadedAt",
table: "GameAssets");
migrationBuilder.AlterColumn<string>(
name: "LocalPath",
table: "GameAssets",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "FileSha256",
table: "GameAssets",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 2, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3366), new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3363) });
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 2, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375), new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375) });
}
}
}

View File

@@ -442,6 +442,9 @@ namespace RR3CommunityServer.Migrations
b.Property<string>("CustomAuthor")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<DateTime>("DownloadedAt")
.HasColumnType("TEXT");
@@ -453,7 +456,6 @@ namespace RR3CommunityServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("FileSha256")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long>("FileSize")
@@ -465,11 +467,13 @@ namespace RR3CommunityServer.Migrations
b.Property<bool>("IsCustomContent")
.HasColumnType("INTEGER");
b.Property<bool>("IsRequired")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastAccessedAt")
.HasColumnType("TEXT");
b.Property<string>("LocalPath")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Md5Hash")
@@ -481,6 +485,9 @@ namespace RR3CommunityServer.Migrations
b.Property<string>("TrackId")
.HasColumnType("TEXT");
b.Property<DateTime>("UploadedAt")
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasColumnType("TEXT");
@@ -704,10 +711,10 @@ namespace RR3CommunityServer.Migrations
Active = true,
CarName = "Any Car",
CashReward = 10000,
EndDate = new DateTime(2026, 2, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3366),
EndDate = new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2221),
GoldReward = 50,
Name = "Daily Sprint Challenge",
StartDate = new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3363),
StartDate = new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2218),
TargetTime = 90.5,
TrackName = "Silverstone National"
},
@@ -717,10 +724,10 @@ namespace RR3CommunityServer.Migrations
Active = true,
CarName = "Any Car",
CashReward = 25000,
EndDate = new DateTime(2026, 2, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375),
EndDate = new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228),
GoldReward = 100,
Name = "Speed Demon Trial",
StartDate = new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375),
StartDate = new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228),
TargetTime = 120.0,
TrackName = "Dubai Autodrome"
});
@@ -797,6 +804,84 @@ namespace RR3CommunityServer.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EmailVerificationToken")
.HasColumnType("TEXT");
b.Property<bool>("EmailVerified")
.HasColumnType("INTEGER");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("PasswordResetExpiry")
.HasColumnType("TEXT");
b.Property<string>("PasswordResetToken")
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Accounts");
});
modelBuilder.Entity("RR3CommunityServer.Models.DeviceAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccountId")
.HasColumnType("INTEGER");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.HasColumnType("TEXT");
b.Property<DateTime>("LastUsedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("LinkedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("DeviceAccounts");
});
modelBuilder.Entity("RR3CommunityServer.Models.UserSettings", b =>
{
b.Property<int>("Id")
@@ -841,12 +926,37 @@ namespace RR3CommunityServer.Migrations
.IsRequired();
});
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Models.DeviceAccount", b =>
{
b.HasOne("RR3CommunityServer.Models.Account", "Account")
.WithMany("LinkedDevices")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Account");
});
modelBuilder.Entity("RR3CommunityServer.Data.User", b =>
{
b.Navigation("CareerProgress");
b.Navigation("OwnedCars");
});
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
{
b.Navigation("LinkedDevices");
});
#pragma warning restore 612, 618
}
}

View File

@@ -0,0 +1,113 @@
using RR3CommunityServer.Data;
namespace RR3CommunityServer.Models;
// Account entity with authentication
public class Account
{
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastLoginAt { get; set; }
public bool IsActive { get; set; } = true;
public bool EmailVerified { get; set; } = false;
public string? EmailVerificationToken { get; set; }
public string? PasswordResetToken { get; set; }
public DateTime? PasswordResetExpiry { get; set; }
// Link to game user data
public int? UserId { get; set; }
public User? User { get; set; }
// Multiple devices can be linked to one account
public List<DeviceAccount> LinkedDevices { get; set; } = new();
}
// Join table for account-device relationship
public class DeviceAccount
{
public int Id { get; set; }
public int AccountId { get; set; }
public Account Account { get; set; } = null!;
public string DeviceId { get; set; } = string.Empty;
public string? DeviceName { get; set; }
public DateTime LinkedAt { get; set; } = DateTime.UtcNow;
public DateTime LastUsedAt { get; set; } = DateTime.UtcNow;
}
// Request/Response DTOs for authentication
public class RegisterRequest
{
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string ConfirmPassword { get; set; } = string.Empty;
}
public class LoginRequest
{
public string UsernameOrEmail { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string? DeviceId { get; set; }
}
public class LoginResponse
{
public string Token { get; set; } = string.Empty;
public int AccountId { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public DateTime ExpiresAt { get; set; }
}
public class ChangePasswordRequest
{
public string CurrentPassword { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
public string ConfirmPassword { get; set; } = string.Empty;
}
public class ForgotPasswordRequest
{
public string Email { get; set; } = string.Empty;
}
public class ResetPasswordRequest
{
public string Token { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
public string ConfirmPassword { get; set; } = string.Empty;
}
public class LinkDeviceRequest
{
public string DeviceId { get; set; } = string.Empty;
public string? DeviceName { get; set; }
}
public class AccountSettingsResponse
{
public int AccountId { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public bool EmailVerified { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public List<LinkedDeviceInfo> LinkedDevices { get; set; } = new();
// Game progress
public int? Gold { get; set; }
public int? Cash { get; set; }
public int? Level { get; set; }
public int? CarsOwned { get; set; }
}
public class LinkedDeviceInfo
{
public string DeviceId { get; set; } = string.Empty;
public string? DeviceName { get; set; }
public DateTime LinkedAt { get; set; }
public DateTime LastUsedAt { get; set; }
}

View File

@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages;
[Authorize]
public class AdminModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -0,0 +1,321 @@
@page
@model RR3CommunityServer.Pages.AssetsModel
@{
ViewData["Title"] = "Asset 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>📦 Asset Management</h1>
<p class="text-muted">Upload and manage game assets for client downloads</p>
</div>
<a href="/admin" class="btn btn-outline-secondary">← Back to Dashboard</a>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Message))
{
<div class="alert alert-@(Model.IsError ? "danger" : "success") alert-dismissible fade show" role="alert">
@Model.Message
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<!-- Asset Statistics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card border-primary">
<div class="card-body text-center">
<h3 class="text-primary">@Model.Stats.TotalAssets</h3>
<p class="mb-0 text-muted">Total Assets</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.Stats.AvailableAssets</h3>
<p class="mb-0 text-muted">Available</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.Stats.TotalSizeMB MB</h3>
<p class="mb-0 text-muted">Total Size</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.Stats.TotalDownloads</h3>
<p class="mb-0 text-muted">Downloads</p>
</div>
</div>
</div>
</div>
<!-- Upload Asset Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">⬆️ Upload New Asset</h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data" asp-page-handler="Upload">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="assetFile" class="form-label">Asset File</label>
<input type="file" class="form-control" id="assetFile" name="assetFile" required>
<small class="text-muted">Supported: .pak, .z, .dat, .nct, .json, .xml, images, audio</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="eaCdnPath" class="form-label">EA CDN Path</label>
<input type="text" class="form-control" id="eaCdnPath" name="eaCdnPath" placeholder="/rr3/assets/file.pak" required>
<small class="text-muted">Path format: /rr3/category/filename.ext</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" name="category" required>
<option value="">Select category...</option>
<option value="base">Base Assets</option>
<option value="cars">Cars</option>
<option value="tracks">Tracks</option>
<option value="audio">Audio</option>
<option value="textures">Textures</option>
<option value="ui">UI</option>
<option value="events">Events</option>
<option value="dlc">DLC</option>
<option value="updates">Updates</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="assetType" class="form-label">Asset Type</label>
<select class="form-select" id="assetType" name="assetType">
<option value="Data">Data File</option>
<option value="Texture">Texture</option>
<option value="Audio">Audio</option>
<option value="Model">3D Model</option>
<option value="Config">Configuration</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label d-block">&nbsp;</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isRequired" name="isRequired" checked>
<label class="form-check-label" for="isRequired">
Required Asset (mandatory download)
</label>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="2" placeholder="Brief description of this asset..."></textarea>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-cloud-upload"></i> Upload Asset
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Asset List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">📋 Asset Inventory</h5>
<div>
<button class="btn btn-sm btn-outline-light" onclick="refreshAssets()">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
<form method="post" asp-page-handler="GenerateManifest" class="d-inline">
<button type="submit" class="btn btn-sm btn-success">
<i class="bi bi-file-text"></i> Generate Manifest
</button>
</form>
</div>
</div>
<div class="card-body">
@if (!Model.Assets.Any())
{
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> No assets uploaded yet. Use the form above to upload your first asset.
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>File Name</th>
<th>EA CDN Path</th>
<th>Category</th>
<th>Type</th>
<th>Size</th>
<th>MD5</th>
<th>Downloads</th>
<th>Required</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var asset in Model.Assets)
{
<tr>
<td>
<strong>@asset.FileName</strong>
@if (!string.IsNullOrEmpty(asset.Description))
{
<br><small class="text-muted">@asset.Description</small>
}
</td>
<td><code>@asset.EaCdnPath</code></td>
<td><span class="badge bg-secondary">@asset.Category</span></td>
<td><span class="badge bg-info">@asset.AssetType</span></td>
<td>@FormatFileSize(asset.FileSize)</td>
<td>
<code class="small">@(asset.Md5Hash?.Substring(0, 8) ?? "N/A")...</code>
@if (!string.IsNullOrEmpty(asset.Md5Hash))
{
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('@asset.Md5Hash')">
<i class="bi bi-clipboard"></i>
</button>
}
</td>
<td>@asset.AccessCount</td>
<td>
@if (asset.IsRequired)
{
<span class="badge bg-danger">Required</span>
}
else
{
<span class="badge bg-secondary">Optional</span>
}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/content/api@asset.EaCdnPath" class="btn btn-outline-primary" target="_blank" title="Download">
<i class="bi bi-download"></i>
</a>
<form method="post" asp-page-handler="Delete" asp-route-id="@asset.Id" class="d-inline"
onsubmit="return confirm('Delete @asset.FileName?')">
<button type="submit" class="btn btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
</div>
</div>
<!-- How Nimble SDK Downloads Assets -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"> Nimble SDK Asset Download System</h5>
</div>
<div class="card-body">
<h6>How RR3 Downloads Assets:</h6>
<ol>
<li><strong>Game Startup:</strong> UnpackAssetsActivity extracts bundled APK assets</li>
<li><strong>Manifest Request:</strong> Game calls <code>GET /content/api/manifest</code></li>
<li><strong>Verification:</strong> Compares local assets with manifest (MD5 checksums)</li>
<li><strong>Download Missing:</strong> Calls <code>GET /content/api/[asset-path]</code> for missing files</li>
<li><strong>Storage:</strong> Saves to <code>/external/storage/apk/</code> directory</li>
<li><strong>Launch Game:</strong> All required assets present, game starts</li>
</ol>
<h6 class="mt-3">Asset Manifest Format:</h6>
<pre><code>{
"resultCode": 0,
"message": "Success",
"data": [
{
"path": "/rr3/base/game_data.pak",
"md5": "a1b2c3d4e5f6...",
"compressedSize": 1048576,
"uncompressedSize": 2097152,
"category": "base"
}
]
}</code></pre>
<h6 class="mt-3">Nimble SDK Authentication Headers:</h6>
<ul>
<li><code>EAM-SESSION</code> - Session UUID</li>
<li><code>EAM-USER-ID</code> - User identifier</li>
<li><code>EA-SELL-ID</code> - Marketplace (e.g., GOOGLE_PLAY)</li>
<li><code>SDK-VERSION</code> - Nimble SDK version</li>
</ul>
<div class="alert alert-warning mt-3">
<strong>Important:</strong> Assets must have correct MD5 hashes or the game will reject them and re-download.
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('Copied to clipboard: ' + text);
});
}
function refreshAssets() {
location.reload();
}
</script>
}
@functions {
private string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}

View File

@@ -0,0 +1,289 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using RR3CommunityServer.Models;
using System.Security.Cryptography;
namespace RR3CommunityServer.Pages;
[Authorize]
public class AssetsModel : PageModel
{
private readonly RR3DbContext _context;
private readonly IConfiguration _configuration;
private readonly ILogger<AssetsModel> _logger;
private readonly string _assetsBasePath;
public AssetsModel(RR3DbContext context, IConfiguration configuration, ILogger<AssetsModel> logger)
{
_context = context;
_configuration = configuration;
_logger = logger;
_assetsBasePath = configuration.GetValue<string>("AssetsBasePath")
?? Path.Combine(Directory.GetCurrentDirectory(), "Assets", "downloaded");
}
public List<GameAsset> Assets { get; set; } = new();
public AssetStats Stats { get; set; } = new();
public string? Message { get; set; }
public bool IsError { get; set; }
public async Task OnGetAsync()
{
Assets = await _context.GameAssets
.OrderByDescending(a => a.UploadedAt)
.ToListAsync();
await CalculateStatsAsync();
}
public async Task<IActionResult> OnPostUploadAsync(
IFormFile assetFile,
string eaCdnPath,
string category,
string assetType,
bool isRequired,
string? description)
{
try
{
if (assetFile == null || assetFile.Length == 0)
{
Message = "No file selected.";
IsError = true;
await OnGetAsync();
return Page();
}
// Ensure assets directory exists
if (!Directory.Exists(_assetsBasePath))
{
Directory.CreateDirectory(_assetsBasePath);
}
// Create category subdirectory
var categoryPath = Path.Combine(_assetsBasePath, category);
if (!Directory.Exists(categoryPath))
{
Directory.CreateDirectory(categoryPath);
}
// Save file to disk
var fileName = Path.GetFileName(assetFile.FileName);
var localPath = Path.Combine(categoryPath, fileName);
using (var stream = new FileStream(localPath, FileMode.Create))
{
await assetFile.CopyToAsync(stream);
}
// Calculate MD5 and SHA256
var md5Hash = await CalculateMd5Async(localPath);
var sha256Hash = await CalculateSha256Async(localPath);
var fileInfo = new FileInfo(localPath);
// Normalize EA CDN path
if (!eaCdnPath.StartsWith("/"))
{
eaCdnPath = "/" + eaCdnPath;
}
// Check if asset already exists
var existingAsset = await _context.GameAssets
.FirstOrDefaultAsync(a => a.EaCdnPath == eaCdnPath);
if (existingAsset != null)
{
// Update existing asset
existingAsset.FileName = fileName;
existingAsset.LocalPath = localPath;
existingAsset.FileSize = fileInfo.Length;
existingAsset.Md5Hash = md5Hash;
existingAsset.FileSha256 = sha256Hash;
existingAsset.Category = category;
existingAsset.AssetType = assetType;
existingAsset.IsRequired = isRequired;
existingAsset.Description = description;
existingAsset.ContentType = GetContentType(fileName);
existingAsset.UploadedAt = DateTime.UtcNow;
Message = $"Asset '{fileName}' updated successfully!";
}
else
{
// Create new asset
var asset = new GameAsset
{
FileName = fileName,
EaCdnPath = eaCdnPath,
LocalPath = localPath,
FileSize = fileInfo.Length,
Md5Hash = md5Hash,
FileSha256 = sha256Hash,
Category = category,
AssetType = assetType,
IsRequired = isRequired,
Description = description,
ContentType = GetContentType(fileName),
UploadedAt = DateTime.UtcNow,
DownloadedAt = DateTime.UtcNow
};
_context.GameAssets.Add(asset);
Message = $"Asset '{fileName}' uploaded successfully!";
}
await _context.SaveChangesAsync();
_logger.LogInformation("Asset uploaded: {FileName} -> {CdnPath}", fileName, eaCdnPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error uploading asset");
Message = $"Error uploading asset: {ex.Message}";
IsError = true;
}
await OnGetAsync();
return Page();
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
try
{
var asset = await _context.GameAssets.FindAsync(id);
if (asset == null)
{
Message = "Asset not found.";
IsError = true;
await OnGetAsync();
return Page();
}
// Delete file from disk
if (!string.IsNullOrEmpty(asset.LocalPath) && System.IO.File.Exists(asset.LocalPath))
{
System.IO.File.Delete(asset.LocalPath);
}
_context.GameAssets.Remove(asset);
await _context.SaveChangesAsync();
Message = $"Asset '{asset.FileName}' deleted successfully!";
_logger.LogInformation("Asset deleted: {FileName}", asset.FileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting asset");
Message = $"Error deleting asset: {ex.Message}";
IsError = true;
}
await OnGetAsync();
return Page();
}
public async Task<IActionResult> OnPostGenerateManifestAsync()
{
try
{
var assets = await _context.GameAssets.ToListAsync();
// Generate manifest in RR3 format (tab-separated)
var manifestContent = new System.Text.StringBuilder();
foreach (var asset in assets)
{
// Format: /path/to/file.ext md5hash compressedSize uncompressedSize
manifestContent.AppendLine($"{asset.EaCdnPath}\t{asset.Md5Hash}\t{asset.CompressedSize ?? asset.FileSize}\t{asset.FileSize}");
}
// Save to Assets directory
var manifestPath = Path.Combine(_assetsBasePath, "asset_list_community.txt");
await System.IO.File.WriteAllTextAsync(manifestPath, manifestContent.ToString());
// Also generate JSON manifest for API
var jsonManifest = assets.Select(a => new
{
path = a.EaCdnPath,
md5 = a.Md5Hash,
compressedSize = a.CompressedSize ?? a.FileSize,
uncompressedSize = a.FileSize,
category = a.Category,
required = a.IsRequired
});
var jsonPath = Path.Combine(_assetsBasePath, "asset_manifest_community.json");
await System.IO.File.WriteAllTextAsync(jsonPath,
System.Text.Json.JsonSerializer.Serialize(jsonManifest, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
Message = $"Manifest generated successfully! ({assets.Count} assets)";
_logger.LogInformation("Asset manifest generated with {Count} assets", assets.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating manifest");
Message = $"Error generating manifest: {ex.Message}";
IsError = true;
}
await OnGetAsync();
return Page();
}
private async Task CalculateStatsAsync()
{
Stats.TotalAssets = Assets.Count;
Stats.AvailableAssets = Assets.Count(a => !string.IsNullOrEmpty(a.LocalPath) && System.IO.File.Exists(a.LocalPath));
Stats.TotalSizeMB = (long)(Assets.Sum(a => a.FileSize) / 1024.0 / 1024.0);
Stats.TotalDownloads = Assets.Sum(a => a.AccessCount);
}
private async Task<string> CalculateMd5Async(string filePath)
{
using var md5 = MD5.Create();
using var stream = System.IO.File.OpenRead(filePath);
var hash = await md5.ComputeHashAsync(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
private async Task<string> CalculateSha256Async(string filePath)
{
using var sha256 = SHA256.Create();
using var stream = System.IO.File.OpenRead(filePath);
var hash = await sha256.ComputeHashAsync(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
private string GetContentType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".pak" => "application/octet-stream",
".dat" => "application/octet-stream",
".nct" => "application/octet-stream",
".z" => "application/x-compress",
".json" => "application/json",
".xml" => "application/xml",
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".pvr" => "image/pvr",
".atlas" => "application/octet-stream",
".mp3" => "audio/mpeg",
".ogg" => "audio/ogg",
".wav" => "audio/wav",
_ => "application/octet-stream"
};
}
}
public class AssetStats
{
public int TotalAssets { get; set; }
public int AvailableAssets { get; set; }
public long TotalSizeMB { get; set; }
public int TotalDownloads { get; set; }
}

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages;
[Authorize]
public class CatalogModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Pages;
[Authorize]
public class DeviceSettingsModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -0,0 +1,164 @@
@page
@model RR3CommunityServer.Pages.LoginModel
@{
ViewData["Title"] = "Login";
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login - RR3 Community Server</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
width: 100%;
max-width: 400px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo h1 {
color: #667eea;
font-size: 28px;
margin-bottom: 5px;
}
.logo p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
color: #333;
font-weight: 500;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.error-message {
background: #fee;
border: 1px solid #fcc;
border-radius: 6px;
padding: 12px;
color: #c33;
margin-bottom: 20px;
font-size: 14px;
}
.btn-login {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-login:active {
transform: translateY(0);
}
.register-link {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 14px;
}
.register-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.register-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<h1>🏎️ RR3 Community Server</h1>
<p>Admin Panel Login</p>
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="error-message">
@Model.ErrorMessage
</div>
}
<form method="post">
@Html.AntiForgeryToken()
<div class="form-group">
<label for="Username">Username or Email</label>
<input type="text" id="Username" name="Username" required autofocus />
</div>
<div class="form-group">
<label for="Password">Password</label>
<input type="password" id="Password" name="Password" required />
</div>
<button type="submit" class="btn-login">Login</button>
</form>
<div class="register-link">
Don't have an account? <a asp-page="/Register">Register here</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using RR3CommunityServer.Services;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Pages;
public class LoginModel : PageModel
{
private readonly IAuthService _authService;
private readonly ILogger<LoginModel> _logger;
public LoginModel(IAuthService authService, ILogger<LoginModel> logger)
{
_authService = authService;
_logger = logger;
}
[BindProperty]
public string Username { get; set; } = string.Empty;
[BindProperty]
public string Password { get; set; } = string.Empty;
public string? ErrorMessage { get; set; }
public void OnGet()
{
// If already logged in, redirect to admin panel
if (User.Identity?.IsAuthenticated == true)
{
Response.Redirect("/admin");
}
}
public async Task<IActionResult> OnPostAsync()
{
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))
{
ErrorMessage = "Username and password are required";
return Page();
}
var loginRequest = new LoginRequest
{
UsernameOrEmail = Username,
Password = Password
};
var (success, response, error) = await _authService.LoginAsync(loginRequest);
if (!success || response == null)
{
ErrorMessage = error ?? "Invalid username or password";
_logger.LogWarning("Failed login attempt for: {Username}", Username);
return Page();
}
// Create authentication cookie
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, response.AccountId.ToString()),
new Claim(ClaimTypes.Name, response.Username),
new Claim(ClaimTypes.Email, response.Email)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true, // Remember me
ExpiresUtc = response.ExpiresAt
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
_logger.LogInformation("User logged in to admin panel: {Username}", response.Username);
return RedirectToPage("/Admin");
}
}

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace RR3CommunityServer.Pages;
public class LogoutModel : PageModel
{
private readonly ILogger<LogoutModel> _logger;
public LogoutModel(ILogger<LogoutModel> logger)
{
_logger = logger;
}
public async Task<IActionResult> OnGetAsync()
{
var username = User.Identity?.Name ?? "Unknown";
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
_logger.LogInformation("User logged out: {Username}", username);
return RedirectToPage("/Login");
}
}

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages;
[Authorize]
public class PurchasesModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -0,0 +1,209 @@
@page
@model RR3CommunityServer.Pages.RegisterModel
@{
ViewData["Title"] = "Register";
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Register - RR3 Community Server</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.register-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
width: 100%;
max-width: 450px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo h1 {
color: #667eea;
font-size: 28px;
margin-bottom: 5px;
}
.logo p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
color: #333;
font-weight: 500;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.error-message {
background: #fee;
border: 1px solid #fcc;
border-radius: 6px;
padding: 12px;
color: #c33;
margin-bottom: 20px;
font-size: 14px;
}
.success-message {
background: #efe;
border: 1px solid #cfc;
border-radius: 6px;
padding: 12px;
color: #363;
margin-bottom: 20px;
font-size: 14px;
}
.btn-register {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-register:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-register:active {
transform: translateY(0);
}
.login-link {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 14px;
}
.login-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.login-link a:hover {
text-decoration: underline;
}
.info-box {
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 6px;
padding: 12px;
margin-bottom: 20px;
font-size: 13px;
color: #1976d2;
}
</style>
</head>
<body>
<div class="register-container">
<div class="logo">
<h1>🏎️ RR3 Community Server</h1>
<p>Create Account</p>
</div>
<div class="info-box">
<strong>Starting Resources:</strong><br>
• 100,000 Gold<br>
• 500,000 Cash<br>
• Access to admin panel
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="error-message">
@Model.ErrorMessage
</div>
}
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
{
<div class="success-message">
@Model.SuccessMessage
</div>
}
<form method="post">
@Html.AntiForgeryToken()
<div class="form-group">
<label for="Username">Username</label>
<input type="text" id="Username" name="Username" required autofocus minlength="3" />
</div>
<div class="form-group">
<label for="Email">Email</label>
<input type="email" id="Email" name="Email" required />
</div>
<div class="form-group">
<label for="Password">Password</label>
<input type="password" id="Password" name="Password" required minlength="6" />
</div>
<div class="form-group">
<label for="ConfirmPassword">Confirm Password</label>
<input type="password" id="ConfirmPassword" name="ConfirmPassword" required minlength="6" />
</div>
<button type="submit" class="btn-register">Create Account</button>
</form>
<div class="login-link">
Already have an account? <a asp-page="/Login">Login here</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,110 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using RR3CommunityServer.Services;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Pages;
public class RegisterModel : PageModel
{
private readonly IAuthService _authService;
private readonly ILogger<RegisterModel> _logger;
public RegisterModel(IAuthService authService, ILogger<RegisterModel> logger)
{
_authService = authService;
_logger = logger;
}
[BindProperty]
public string Username { get; set; } = string.Empty;
[BindProperty]
public string Email { get; set; } = string.Empty;
[BindProperty]
public string Password { get; set; } = string.Empty;
[BindProperty]
public string ConfirmPassword { get; set; } = string.Empty;
public string? ErrorMessage { get; set; }
public string? SuccessMessage { get; set; }
public void OnGet()
{
// If already logged in, redirect to admin panel
if (User.Identity?.IsAuthenticated == true)
{
Response.Redirect("/admin");
}
}
public async Task<IActionResult> OnPostAsync()
{
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Email) ||
string.IsNullOrWhiteSpace(Password) || string.IsNullOrWhiteSpace(ConfirmPassword))
{
ErrorMessage = "All fields are required";
return Page();
}
var registerRequest = new RegisterRequest
{
Username = Username,
Email = Email,
Password = Password,
ConfirmPassword = ConfirmPassword
};
var (success, token, error) = await _authService.RegisterAsync(registerRequest);
if (!success || string.IsNullOrEmpty(token))
{
ErrorMessage = error ?? "Registration failed";
_logger.LogWarning("Failed registration attempt for: {Username}", Username);
return Page();
}
_logger.LogInformation("New account registered: {Username} ({Email})", Username, Email);
// Auto-login after registration
var loginRequest = new LoginRequest
{
UsernameOrEmail = Username,
Password = Password
};
var (loginSuccess, response, loginError) = await _authService.LoginAsync(loginRequest);
if (loginSuccess && response != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, response.AccountId.ToString()),
new Claim(ClaimTypes.Name, response.Username),
new Claim(ClaimTypes.Email, response.Email)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = response.ExpiresAt
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return RedirectToPage("/Admin");
}
SuccessMessage = "Account created successfully! Please login.";
return RedirectToPage("/Login");
}
}

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages;
[Authorize]
public class RewardsModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages;
[Authorize]
public class SessionsModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
namespace RR3CommunityServer.Pages;
[Authorize]
public class SettingsModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages;
[Authorize]
public class UsersModel : PageModel
{
private readonly RR3DbContext _context;

View File

@@ -103,11 +103,29 @@
<i class="bi bi-cart"></i> Purchases
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/assets">
<i class="bi bi-box-seam"></i> Assets
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">
<i class="bi bi-gear"></i> Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/swagger" target="_blank">
<i class="bi bi-code-slash"></i> API
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> @User.Identity?.Name
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="/Logout"><i class="bi bi-box-arrow-right"></i> Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.Cookies;
using RR3CommunityServer.Data;
using RR3CommunityServer.Services;
using RR3CommunityServer.Middleware;
@@ -8,6 +9,20 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddRazorPages(); // Add Razor Pages support
// Add cookie authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
options.AccessDeniedPath = "/Login";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
});
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
@@ -20,6 +35,7 @@ builder.Services.AddScoped<ISessionService, SessionService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ICatalogService, CatalogService>();
builder.Services.AddScoped<IDrmService, DrmService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<AssetExtractionService>();
// CORS for cross-origin requests
@@ -52,16 +68,19 @@ using (var scope = app.Services.CreateScope())
app.UseHttpsRedirection();
app.UseCors();
// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();
// 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"));
// Redirect root to login page
app.MapGet("/", () => Results.Redirect("/Login"));
Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Real Racing 3 Community Server - RUNNING ║");

View File

@@ -16,6 +16,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,380 @@
using System.Security.Cryptography;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Services;
public interface IAuthService
{
Task<(bool Success, string? Token, string? Error)> RegisterAsync(RegisterRequest request);
Task<(bool Success, LoginResponse? Response, string? Error)> LoginAsync(LoginRequest request);
Task<(bool Success, string? Error)> ChangePasswordAsync(int accountId, ChangePasswordRequest request);
Task<(bool Success, string? Error)> ForgotPasswordAsync(ForgotPasswordRequest request);
Task<(bool Success, string? Error)> ResetPasswordAsync(ResetPasswordRequest request);
Task<(bool Success, string? Error)> LinkDeviceAsync(int accountId, LinkDeviceRequest request);
Task<(bool Success, string? Error)> UnlinkDeviceAsync(int accountId, string deviceId);
Task<AccountSettingsResponse?> GetAccountSettingsAsync(int accountId);
Task<Account?> ValidateTokenAsync(string token);
}
public class AuthService : IAuthService
{
private readonly RR3DbContext _context;
private readonly IConfiguration _configuration;
private readonly ILogger<AuthService> _logger;
public AuthService(RR3DbContext context, IConfiguration configuration, ILogger<AuthService> logger)
{
_context = context;
_configuration = configuration;
_logger = logger;
}
public async Task<(bool Success, string? Token, string? Error)> RegisterAsync(RegisterRequest request)
{
// Validate input
if (string.IsNullOrWhiteSpace(request.Username) || request.Username.Length < 3)
return (false, null, "Username must be at least 3 characters");
if (string.IsNullOrWhiteSpace(request.Email) || !request.Email.Contains('@'))
return (false, null, "Invalid email address");
if (string.IsNullOrWhiteSpace(request.Password) || request.Password.Length < 6)
return (false, null, "Password must be at least 6 characters");
if (request.Password != request.ConfirmPassword)
return (false, null, "Passwords do not match");
// Check if username or email already exists
var existingUsername = await _context.Set<Account>()
.AnyAsync(a => a.Username.ToLower() == request.Username.ToLower());
if (existingUsername)
return (false, null, "Username already taken");
var existingEmail = await _context.Set<Account>()
.AnyAsync(a => a.Email.ToLower() == request.Email.ToLower());
if (existingEmail)
return (false, null, "Email already registered");
// Create account
var account = new Account
{
Username = request.Username,
Email = request.Email,
PasswordHash = HashPassword(request.Password),
CreatedAt = DateTime.UtcNow,
IsActive = true,
EmailVerified = false,
EmailVerificationToken = GenerateToken()
};
// Create associated game user
var user = new User
{
SynergyId = Guid.NewGuid().ToString(),
Nickname = request.Username,
Gold = 100000, // Starting gold for community server
Cash = 500000, // Starting cash
Level = 1,
Experience = 0,
Reputation = 0,
CreatedAt = DateTime.UtcNow
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
account.UserId = user.Id;
_context.Set<Account>().Add(account);
await _context.SaveChangesAsync();
_logger.LogInformation("New account registered: {Username} ({Email})", account.Username, account.Email);
// Generate JWT token
var token = GenerateJwtToken(account);
return (true, token, null);
}
public async Task<(bool Success, LoginResponse? Response, string? Error)> LoginAsync(LoginRequest request)
{
// Find account by username or email
var account = await _context.Set<Account>()
.Include(a => a.User)
.FirstOrDefaultAsync(a =>
a.Username.ToLower() == request.UsernameOrEmail.ToLower() ||
a.Email.ToLower() == request.UsernameOrEmail.ToLower());
if (account == null)
return (false, null, "Invalid username/email or password");
if (!account.IsActive)
return (false, null, "Account is disabled");
// Verify password
if (!VerifyPassword(request.Password, account.PasswordHash))
return (false, null, "Invalid username/email or password");
// Update last login
account.LastLoginAt = DateTime.UtcNow;
// Link device if provided
if (!string.IsNullOrEmpty(request.DeviceId))
{
var deviceLink = await _context.Set<DeviceAccount>()
.FirstOrDefaultAsync(da => da.AccountId == account.Id && da.DeviceId == request.DeviceId);
if (deviceLink == null)
{
deviceLink = new DeviceAccount
{
AccountId = account.Id,
DeviceId = request.DeviceId,
LinkedAt = DateTime.UtcNow,
LastUsedAt = DateTime.UtcNow
};
_context.Set<DeviceAccount>().Add(deviceLink);
}
else
{
deviceLink.LastUsedAt = DateTime.UtcNow;
}
}
await _context.SaveChangesAsync();
_logger.LogInformation("User logged in: {Username}", account.Username);
// Generate JWT token
var token = GenerateJwtToken(account);
var expiresAt = DateTime.UtcNow.AddDays(30);
var response = new LoginResponse
{
Token = token,
AccountId = account.Id,
Username = account.Username,
Email = account.Email,
ExpiresAt = expiresAt
};
return (true, response, null);
}
public async Task<(bool Success, string? Error)> ChangePasswordAsync(int accountId, ChangePasswordRequest request)
{
var account = await _context.Set<Account>().FindAsync(accountId);
if (account == null)
return (false, "Account not found");
if (!VerifyPassword(request.CurrentPassword, account.PasswordHash))
return (false, "Current password is incorrect");
if (request.NewPassword.Length < 6)
return (false, "New password must be at least 6 characters");
if (request.NewPassword != request.ConfirmPassword)
return (false, "Passwords do not match");
account.PasswordHash = HashPassword(request.NewPassword);
await _context.SaveChangesAsync();
_logger.LogInformation("Password changed for account: {AccountId}", accountId);
return (true, null);
}
public async Task<(bool Success, string? Error)> ForgotPasswordAsync(ForgotPasswordRequest request)
{
var account = await _context.Set<Account>()
.FirstOrDefaultAsync(a => a.Email.ToLower() == request.Email.ToLower());
if (account == null)
{
// Don't reveal if email exists
_logger.LogWarning("Password reset requested for non-existent email: {Email}", request.Email);
return (true, null);
}
account.PasswordResetToken = GenerateToken();
account.PasswordResetExpiry = DateTime.UtcNow.AddHours(24);
await _context.SaveChangesAsync();
_logger.LogInformation("Password reset token generated for: {Email}", request.Email);
// TODO: Send email with reset link
// For now, just log the token
_logger.LogWarning("Password reset token: {Token} (implement email service)", account.PasswordResetToken);
return (true, null);
}
public async Task<(bool Success, string? Error)> ResetPasswordAsync(ResetPasswordRequest request)
{
var account = await _context.Set<Account>()
.FirstOrDefaultAsync(a => a.PasswordResetToken == request.Token);
if (account == null || account.PasswordResetExpiry == null || account.PasswordResetExpiry < DateTime.UtcNow)
return (false, "Invalid or expired reset token");
if (request.NewPassword.Length < 6)
return (false, "Password must be at least 6 characters");
if (request.NewPassword != request.ConfirmPassword)
return (false, "Passwords do not match");
account.PasswordHash = HashPassword(request.NewPassword);
account.PasswordResetToken = null;
account.PasswordResetExpiry = null;
await _context.SaveChangesAsync();
_logger.LogInformation("Password reset completed for account: {AccountId}", account.Id);
return (true, null);
}
public async Task<(bool Success, string? Error)> LinkDeviceAsync(int accountId, LinkDeviceRequest request)
{
var account = await _context.Set<Account>().FindAsync(accountId);
if (account == null)
return (false, "Account not found");
var existingLink = await _context.Set<DeviceAccount>()
.FirstOrDefaultAsync(da => da.AccountId == accountId && da.DeviceId == request.DeviceId);
if (existingLink != null)
return (false, "Device already linked");
var deviceLink = new DeviceAccount
{
AccountId = accountId,
DeviceId = request.DeviceId,
DeviceName = request.DeviceName,
LinkedAt = DateTime.UtcNow,
LastUsedAt = DateTime.UtcNow
};
_context.Set<DeviceAccount>().Add(deviceLink);
await _context.SaveChangesAsync();
_logger.LogInformation("Device {DeviceId} linked to account {AccountId}", request.DeviceId, accountId);
return (true, null);
}
public async Task<(bool Success, string? Error)> UnlinkDeviceAsync(int accountId, string deviceId)
{
var deviceLink = await _context.Set<DeviceAccount>()
.FirstOrDefaultAsync(da => da.AccountId == accountId && da.DeviceId == deviceId);
if (deviceLink == null)
return (false, "Device not linked to this account");
_context.Set<DeviceAccount>().Remove(deviceLink);
await _context.SaveChangesAsync();
_logger.LogInformation("Device {DeviceId} unlinked from account {AccountId}", deviceId, accountId);
return (true, null);
}
public async Task<AccountSettingsResponse?> GetAccountSettingsAsync(int accountId)
{
var account = await _context.Set<Account>()
.Include(a => a.User)
.Include(a => a.LinkedDevices)
.FirstOrDefaultAsync(a => a.Id == accountId);
if (account == null)
return null;
var carsOwned = account.UserId.HasValue
? await _context.OwnedCars.CountAsync(c => c.UserId == account.UserId.Value)
: 0;
return new AccountSettingsResponse
{
AccountId = account.Id,
Username = account.Username,
Email = account.Email,
EmailVerified = account.EmailVerified,
CreatedAt = account.CreatedAt,
LastLoginAt = account.LastLoginAt,
LinkedDevices = account.LinkedDevices.Select(d => new LinkedDeviceInfo
{
DeviceId = d.DeviceId,
DeviceName = d.DeviceName,
LinkedAt = d.LinkedAt,
LastUsedAt = d.LastUsedAt
}).ToList(),
Gold = account.User?.Gold ?? 0,
Cash = account.User?.Cash ?? 0,
Level = account.User?.Level ?? 1,
CarsOwned = carsOwned
};
}
public async Task<Account?> ValidateTokenAsync(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"] ?? "RR3CommunityServer_DefaultSecret_ChangeThis");
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var accountId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
return await _context.Set<Account>().FindAsync(accountId);
}
catch
{
return null;
}
}
private string HashPassword(string password)
{
// Use BCrypt for password hashing
return BCrypt.Net.BCrypt.HashPassword(password);
}
private bool VerifyPassword(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
private string GenerateJwtToken(Account account)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"] ?? "RR3CommunityServer_DefaultSecret_ChangeThis");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("id", account.Id.ToString()),
new Claim("username", account.Username),
new Claim("email", account.Email)
}),
Expires = DateTime.UtcNow.AddDays(30),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private string GenerateToken()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
}
}

View File

@@ -6,6 +6,12 @@
}
},
"AllowedHosts": "*",
"Jwt": {
"Secret": "RR3CommunityServer_SecureJwtSecret_ChangeThisInProduction_MinimumLength32Characters",
"Issuer": "RR3CommunityServer",
"Audience": "RR3Community",
"ExpiryDays": 30
},
"AssetsBasePath": "Assets/downloaded",
"CustomAssetsPath": "Assets/custom",
"ModsPath": "Assets/mods",

View File

@@ -8,17 +8,28 @@
".NETCoreApp,Version=v8.0": {
"RR3CommunityServer/1.0.0": {
"dependencies": {
"BCrypt.Net-Next": "4.0.3",
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.11",
"Microsoft.AspNetCore.OpenApi": "8.0.24",
"Microsoft.EntityFrameworkCore": "8.0.11",
"Microsoft.EntityFrameworkCore.Design": "8.0.11",
"Microsoft.EntityFrameworkCore.Sqlite": "8.0.11",
"Microsoft.Extensions.Http": "10.0.3",
"Swashbuckle.AspNetCore": "6.6.2"
"Swashbuckle.AspNetCore": "6.6.2",
"System.IdentityModel.Tokens.Jwt": "8.2.1"
},
"runtime": {
"RR3CommunityServer.dll": {}
}
},
"BCrypt.Net-Next/4.0.3": {
"runtime": {
"lib/net6.0/BCrypt.Net-Next.dll": {
"assemblyVersion": "4.0.3.0",
"fileVersion": "4.0.3.0"
}
}
},
"Humanizer.Core/2.14.1": {
"runtime": {
"lib/net6.0/Humanizer.dll": {
@@ -27,6 +38,17 @@
}
}
},
"Microsoft.AspNetCore.Authentication.JwtBearer/8.0.11": {
"dependencies": {
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "7.1.2"
},
"runtime": {
"lib/net8.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52116"
}
}
},
"Microsoft.AspNetCore.OpenApi/8.0.24": {
"dependencies": {
"Microsoft.OpenApi": "1.6.14"
@@ -513,6 +535,71 @@
}
}
},
"Microsoft.IdentityModel.Abstractions/8.2.1": {
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Abstractions.dll": {
"assemblyVersion": "8.2.1.0",
"fileVersion": "8.2.1.51115"
}
}
},
"Microsoft.IdentityModel.JsonWebTokens/8.2.1": {
"dependencies": {
"Microsoft.IdentityModel.Tokens": "8.2.1"
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.dll": {
"assemblyVersion": "8.2.1.0",
"fileVersion": "8.2.1.51115"
}
}
},
"Microsoft.IdentityModel.Logging/8.2.1": {
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "8.2.1"
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Logging.dll": {
"assemblyVersion": "8.2.1.0",
"fileVersion": "8.2.1.51115"
}
}
},
"Microsoft.IdentityModel.Protocols/7.1.2": {
"dependencies": {
"Microsoft.IdentityModel.Logging": "8.2.1",
"Microsoft.IdentityModel.Tokens": "8.2.1"
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Protocols.dll": {
"assemblyVersion": "7.1.2.0",
"fileVersion": "7.1.2.41121"
}
}
},
"Microsoft.IdentityModel.Protocols.OpenIdConnect/7.1.2": {
"dependencies": {
"Microsoft.IdentityModel.Protocols": "7.1.2",
"System.IdentityModel.Tokens.Jwt": "8.2.1"
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": {
"assemblyVersion": "7.1.2.0",
"fileVersion": "7.1.2.41121"
}
}
},
"Microsoft.IdentityModel.Tokens/8.2.1": {
"dependencies": {
"Microsoft.IdentityModel.Logging": "8.2.1"
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Tokens.dll": {
"assemblyVersion": "8.2.1.0",
"fileVersion": "8.2.1.51115"
}
}
},
"Microsoft.OpenApi/1.6.14": {
"runtime": {
"lib/netstandard2.0/Microsoft.OpenApi.dll": {
@@ -779,6 +866,18 @@
"fileVersion": "10.0.326.7603"
}
}
},
"System.IdentityModel.Tokens.Jwt/8.2.1": {
"dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "8.2.1",
"Microsoft.IdentityModel.Tokens": "8.2.1"
},
"runtime": {
"lib/net8.0/System.IdentityModel.Tokens.Jwt.dll": {
"assemblyVersion": "8.2.1.0",
"fileVersion": "8.2.1.51115"
}
}
}
}
},
@@ -788,6 +887,13 @@
"serviceable": false,
"sha512": ""
},
"BCrypt.Net-Next/4.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-W+U9WvmZQgi5cX6FS5GDtDoPzUCV4LkBLkywq/kRZhuDwcbavOzcDAr3LXJFqHUi952Yj3LEYoWW0jbEUQChsA==",
"path": "bcrypt.net-next/4.0.3",
"hashPath": "bcrypt.net-next.4.0.3.nupkg.sha512"
},
"Humanizer.Core/2.14.1": {
"type": "package",
"serviceable": true,
@@ -795,6 +901,13 @@
"path": "humanizer.core/2.14.1",
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
},
"Microsoft.AspNetCore.Authentication.JwtBearer/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-9KhRuywosM24BPf1R5erwsvIkpRUu1+btVyOPlM3JgrhFVP4pq5Fuzi3vjP01OHXfbCtNhWa+HGkZeqaWdcO5w==",
"path": "microsoft.aspnetcore.authentication.jwtbearer/8.0.11",
"hashPath": "microsoft.aspnetcore.authentication.jwtbearer.8.0.11.nupkg.sha512"
},
"Microsoft.AspNetCore.OpenApi/8.0.24": {
"type": "package",
"serviceable": true,
@@ -991,6 +1104,48 @@
"path": "microsoft.extensions.primitives/10.0.3",
"hashPath": "microsoft.extensions.primitives.10.0.3.nupkg.sha512"
},
"Microsoft.IdentityModel.Abstractions/8.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-8sMlmHhh5HdP3+yCSCUpJpN1yYrJ6J/V39df9siY8PeMckRMrSBRL/TMs/Jex6P1ly/Ie2mFqvhcPHHrNmCd/w==",
"path": "microsoft.identitymodel.abstractions/8.2.1",
"hashPath": "microsoft.identitymodel.abstractions.8.2.1.nupkg.sha512"
},
"Microsoft.IdentityModel.JsonWebTokens/8.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Oo0SBOzK6p3YIUcc1YTJCaYezVUa5HyUJ/AAB35QwxhhD6Blei5tNjNYDR0IbqHdb5EPUIiKcIbQGoj2b1mIbg==",
"path": "microsoft.identitymodel.jsonwebtokens/8.2.1",
"hashPath": "microsoft.identitymodel.jsonwebtokens.8.2.1.nupkg.sha512"
},
"Microsoft.IdentityModel.Logging/8.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-EgSEAtBoWBynACdhKnMlVAFGGWqOIdmbpW7Vvx2SQ7u7ogZ50NcEGSoGljEsQoGIRYpo0UxXYktKcYMp+G/Bcg==",
"path": "microsoft.identitymodel.logging/8.2.1",
"hashPath": "microsoft.identitymodel.logging.8.2.1.nupkg.sha512"
},
"Microsoft.IdentityModel.Protocols/7.1.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==",
"path": "microsoft.identitymodel.protocols/7.1.2",
"hashPath": "microsoft.identitymodel.protocols.7.1.2.nupkg.sha512"
},
"Microsoft.IdentityModel.Protocols.OpenIdConnect/7.1.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==",
"path": "microsoft.identitymodel.protocols.openidconnect/7.1.2",
"hashPath": "microsoft.identitymodel.protocols.openidconnect.7.1.2.nupkg.sha512"
},
"Microsoft.IdentityModel.Tokens/8.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-oQeLWCATuVXOCdIvouM4GG2xl1YNng+uAxYwu7CG6RuW+y+1+slXrOBq5csTU2pnV2SH3B1GmugDf6Jv/lexjw==",
"path": "microsoft.identitymodel.tokens/8.2.1",
"hashPath": "microsoft.identitymodel.tokens.8.2.1.nupkg.sha512"
},
"Microsoft.OpenApi/1.6.14": {
"type": "package",
"serviceable": true,
@@ -1116,6 +1271,13 @@
"sha512": "sha512-IuZXyF3K5X+mCsBKIQ87Cn/V4Nyb39vyCbzfH/AkoneSWNV/ExGQ/I0m4CEaVAeFh9fW6kp2NVObkmevd1Ys7A==",
"path": "system.diagnostics.diagnosticsource/10.0.3",
"hashPath": "system.diagnostics.diagnosticsource.10.0.3.nupkg.sha512"
},
"System.IdentityModel.Tokens.Jwt/8.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-GVQmbjr2N8awFWPTWyThLxgKnFINObG1P+oX7vFrBY8um3V7V7Dh3wnxaGxNH6v6lSTeVQrY+SaUUBX9H3TPcw==",
"path": "system.identitymodel.tokens.jwt/8.2.1",
"hashPath": "system.identitymodel.tokens.jwt.8.2.1.nupkg.sha512"
}
}
}

View File

@@ -5,5 +5,24 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Jwt": {
"Secret": "RR3CommunityServer_SecureJwtSecret_ChangeThisInProduction_MinimumLength32Characters",
"Issuer": "RR3CommunityServer",
"Audience": "RR3Community",
"ExpiryDays": 30
},
"AssetsBasePath": "Assets/downloaded",
"CustomAssetsPath": "Assets/custom",
"ModsPath": "Assets/mods",
"ServerSettings": {
"AllowSelfSignedCerts": true,
"EnableAssetDownloads": true,
"FreeGoldPurchases": true,
"UnlockAllCars": false,
"UnlimitedCurrency": false,
"EnableModding": true,
"MaxCustomCarUploadSizeMB": 100,
"MaxCustomTrackUploadSizeMB": 200
}
}

Binary file not shown.

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("RR3CommunityServer")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+a7d33090ad47352946904dd2332b4a6c15e225ee")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f1d0d43cb70abf0fc44598a01a6250f3b7b73922")]
[assembly: System.Reflection.AssemblyProductAttribute("RR3CommunityServer")]
[assembly: System.Reflection.AssemblyTitleAttribute("RR3CommunityServer")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
a9e857267e797d27d55007236bf2e0f3befeb9ad1a31a95f91c42d4df2f35dc7
42ee391f7017f761577d9436f4160570943e6c116049654eb80a0c5412651e8a

View File

@@ -26,14 +26,30 @@ build_property.EnableCodeStyleSeverity =
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcQWRtaW4uY3NodG1s
build_metadata.AdditionalFiles.CssScope =
[E:/rr3/RR3CommunityServer/RR3CommunityServer/Pages/Assets.cshtml]
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcQXNzZXRzLmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope =
[E:/rr3/RR3CommunityServer/RR3CommunityServer/Pages/Catalog.cshtml]
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcQ2F0YWxvZy5jc2h0bWw=
build_metadata.AdditionalFiles.CssScope =
[E:/rr3/RR3CommunityServer/RR3CommunityServer/Pages/DeviceSettings.cshtml]
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcRGV2aWNlU2V0dGluZ3MuY3NodG1s
build_metadata.AdditionalFiles.CssScope =
[E:/rr3/RR3CommunityServer/RR3CommunityServer/Pages/Login.cshtml]
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcTG9naW4uY3NodG1s
build_metadata.AdditionalFiles.CssScope =
[E:/rr3/RR3CommunityServer/RR3CommunityServer/Pages/Purchases.cshtml]
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcUHVyY2hhc2VzLmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope =
[E:/rr3/RR3CommunityServer/RR3CommunityServer/Pages/Register.cshtml]
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcUmVnaXN0ZXIuY3NodG1s
build_metadata.AdditionalFiles.CssScope =
[E:/rr3/RR3CommunityServer/RR3CommunityServer/Pages/Rewards.cshtml]
build_metadata.AdditionalFiles.TargetPath = UGFnZXNcUmV3YXJkcy5jc2h0bWw=
build_metadata.AdditionalFiles.CssScope =

View File

@@ -1 +1 @@
ef2a9acb1383590916d3f16acb1a638605828a26b186fcd899f4f16268addae3
710d43ac46614dc8b6a0b200c2dda2aea7d3365426e586abb3f00369358fbe59

View File

@@ -148,3 +148,12 @@ E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.Extensio
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.Extensions.Options.ConfigurationExtensions.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.Extensions.Primitives.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\System.Diagnostics.DiagnosticSource.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\BCrypt.Net-Next.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.AspNetCore.Authentication.JwtBearer.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.IdentityModel.Abstractions.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.IdentityModel.JsonWebTokens.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.IdentityModel.Logging.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.IdentityModel.Protocols.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\Microsoft.IdentityModel.Tokens.dll
E:\rr3\RR3CommunityServer\RR3CommunityServer\bin\Debug\net8.0\System.IdentityModel.Tokens.Jwt.dll

View File

@@ -1 +1 @@
{"documents":{"E:\\rr3\\RR3CommunityServer\\*":"https://raw.githubusercontent.com/ssfdre38/rr3-server/a7d33090ad47352946904dd2332b4a6c15e225ee/*"}}
{"documents":{"E:\\rr3\\RR3CommunityServer\\*":"https://raw.githubusercontent.com/ssfdre38/rr3-server/f1d0d43cb70abf0fc44598a01a6250f3b7b73922/*"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"gdYA/PLOQysRMD9wt3+IrqBqQw0g/GZFOcojepf8P6w=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["7Gcs8uTS1W2TjgmuuoBwaL/zy\u002B2wcKht3msEI7xtxEM=","7jtBYhuQJ4x3LjlWmm4U\u002B5\u002BZ9MCjWKhEdzRCcW1ILdA=","A3Op/M2RFQpYBjcrogPFz1XIhJgm4S0j42sTu7EvHxI=","hnhSRoeFpk3C6XWICUlX/lNip6TfbZWFYZv4weSCyrw=","EoVh8vBcGohUnEMEoZuTXrpZ9uBDHT19VmDHc/D\u002Bm0I=","IdEjAFCVk3xZYjiEMESONot/jkvTj/gnwS5nnpGaIMc=","JVRe\u002Be2d47FunIfxVYRpqRFtljZ8gqrK3xMRy6TCd\u002BQ=","DQG0T8n9f5ohwv9akihU55D4/3WR7\u002BlDnvkdsAHHSgc=","VxDQNRQXYUU41o9SG4HrkKWR59FJIv8lmnwBolB/wE0=","0qcd51IQrNKYL9233q2L9h8dLzPcor56mdtkcOdQWoI=","0Slg2/xnc5E9nXprYyph/57wQou\u002BhGSGgKchbo4aNOg="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"gdYA/PLOQysRMD9wt3+IrqBqQw0g/GZFOcojepf8P6w=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["7Gcs8uTS1W2TjgmuuoBwaL/zy\u002B2wcKht3msEI7xtxEM=","UWedSjPPgrw4tts2Bk2ce0nYJfnBV9zMYOAjYg0PED8=","GecKXPxV0EAagvAtrRNTytwMtFCxZmgKm9sjLyEe8oI=","AD8WKv0o3OeySN/Mlu5s1a4y3Dt/ik0jFKKHCrGjFtA=","hnhSRoeFpk3C6XWICUlX/lNip6TfbZWFYZv4weSCyrw=","fVR30KYkDSf6Wvsw9TujzlqruhwIMbw1wHxa1z/mksA=","VpFNnyDFqynPhhZPcyqeWcncA9QmAv\u002BG3ez5PxfzaTQ=","EoVh8vBcGohUnEMEoZuTXrpZ9uBDHT19VmDHc/D\u002Bm0I=","eB7z8UswjcYO/RErEzjxxHwVLVba/7iPOUH17NS53Fw=","IdEjAFCVk3xZYjiEMESONot/jkvTj/gnwS5nnpGaIMc=","JVRe\u002Be2d47FunIfxVYRpqRFtljZ8gqrK3xMRy6TCd\u002BQ=","DQG0T8n9f5ohwv9akihU55D4/3WR7\u002BlDnvkdsAHHSgc=","VxDQNRQXYUU41o9SG4HrkKWR59FJIv8lmnwBolB/wE0=","x88k5Bg2fv\u002Bie1eIqFd4doOTQY0lwCNPv/5eJfhIK\u002Bw=","0Slg2/xnc5E9nXprYyph/57wQou\u002BhGSGgKchbo4aNOg="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -51,6 +51,14 @@
"net8.0": {
"targetAlias": "net8.0",
"dependencies": {
"BCrypt.Net-Next": {
"target": "Package",
"version": "[4.0.3, )"
},
"Microsoft.AspNetCore.Authentication.JwtBearer": {
"target": "Package",
"version": "[8.0.11, )"
},
"Microsoft.AspNetCore.OpenApi": {
"target": "Package",
"version": "[8.0.24, )"
@@ -76,6 +84,10 @@
"Swashbuckle.AspNetCore": {
"target": "Package",
"version": "[6.6.2, )"
},
"System.IdentityModel.Tokens.Jwt": {
"target": "Package",
"version": "[8.2.1, )"
}
},
"imports": [

View File

@@ -2,6 +2,19 @@
"version": 3,
"targets": {
"net8.0": {
"BCrypt.Net-Next/4.0.3": {
"type": "package",
"compile": {
"lib/net6.0/BCrypt.Net-Next.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net6.0/BCrypt.Net-Next.dll": {
"related": ".xml"
}
}
},
"Humanizer.Core/2.14.1": {
"type": "package",
"compile": {
@@ -15,6 +28,25 @@
}
}
},
"Microsoft.AspNetCore.Authentication.JwtBearer/8.0.11": {
"type": "package",
"dependencies": {
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "7.1.2"
},
"compile": {
"lib/net8.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll": {
"related": ".xml"
}
},
"frameworkReferences": [
"Microsoft.AspNetCore.App"
]
},
"Microsoft.AspNetCore.OpenApi/8.0.24": {
"type": "package",
"dependencies": {
@@ -753,6 +785,101 @@
"buildTransitive/net8.0/_._": {}
}
},
"Microsoft.IdentityModel.Abstractions/8.2.1": {
"type": "package",
"compile": {
"lib/net8.0/Microsoft.IdentityModel.Abstractions.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Abstractions.dll": {
"related": ".xml"
}
}
},
"Microsoft.IdentityModel.JsonWebTokens/8.2.1": {
"type": "package",
"dependencies": {
"Microsoft.IdentityModel.Tokens": "8.2.1"
},
"compile": {
"lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.dll": {
"related": ".xml"
}
}
},
"Microsoft.IdentityModel.Logging/8.2.1": {
"type": "package",
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "8.2.1"
},
"compile": {
"lib/net8.0/Microsoft.IdentityModel.Logging.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Logging.dll": {
"related": ".xml"
}
}
},
"Microsoft.IdentityModel.Protocols/7.1.2": {
"type": "package",
"dependencies": {
"Microsoft.IdentityModel.Logging": "7.1.2",
"Microsoft.IdentityModel.Tokens": "7.1.2"
},
"compile": {
"lib/net8.0/Microsoft.IdentityModel.Protocols.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Protocols.dll": {
"related": ".xml"
}
}
},
"Microsoft.IdentityModel.Protocols.OpenIdConnect/7.1.2": {
"type": "package",
"dependencies": {
"Microsoft.IdentityModel.Protocols": "7.1.2",
"System.IdentityModel.Tokens.Jwt": "7.1.2"
},
"compile": {
"lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": {
"related": ".xml"
}
}
},
"Microsoft.IdentityModel.Tokens/8.2.1": {
"type": "package",
"dependencies": {
"Microsoft.IdentityModel.Logging": "8.2.1"
},
"compile": {
"lib/net8.0/Microsoft.IdentityModel.Tokens.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/Microsoft.IdentityModel.Tokens.dll": {
"related": ".xml"
}
}
},
"Microsoft.OpenApi/1.6.14": {
"type": "package",
"compile": {
@@ -1122,6 +1249,23 @@
"buildTransitive/net8.0/_._": {}
}
},
"System.IdentityModel.Tokens.Jwt/8.2.1": {
"type": "package",
"dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "8.2.1",
"Microsoft.IdentityModel.Tokens": "8.2.1"
},
"compile": {
"lib/net8.0/System.IdentityModel.Tokens.Jwt.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net8.0/System.IdentityModel.Tokens.Jwt.dll": {
"related": ".xml"
}
}
},
"System.IO.Pipelines/6.0.3": {
"type": "package",
"compile": {
@@ -1226,6 +1370,37 @@
}
},
"libraries": {
"BCrypt.Net-Next/4.0.3": {
"sha512": "W+U9WvmZQgi5cX6FS5GDtDoPzUCV4LkBLkywq/kRZhuDwcbavOzcDAr3LXJFqHUi952Yj3LEYoWW0jbEUQChsA==",
"type": "package",
"path": "bcrypt.net-next/4.0.3",
"files": [
".nupkg.metadata",
".signature.p7s",
"bcrypt.net-next.4.0.3.nupkg.sha512",
"bcrypt.net-next.nuspec",
"ico.png",
"lib/net20/BCrypt.Net-Next.dll",
"lib/net20/BCrypt.Net-Next.xml",
"lib/net35/BCrypt.Net-Next.dll",
"lib/net35/BCrypt.Net-Next.xml",
"lib/net462/BCrypt.Net-Next.dll",
"lib/net462/BCrypt.Net-Next.xml",
"lib/net472/BCrypt.Net-Next.dll",
"lib/net472/BCrypt.Net-Next.xml",
"lib/net48/BCrypt.Net-Next.dll",
"lib/net48/BCrypt.Net-Next.xml",
"lib/net5.0/BCrypt.Net-Next.dll",
"lib/net5.0/BCrypt.Net-Next.xml",
"lib/net6.0/BCrypt.Net-Next.dll",
"lib/net6.0/BCrypt.Net-Next.xml",
"lib/netstandard2.0/BCrypt.Net-Next.dll",
"lib/netstandard2.0/BCrypt.Net-Next.xml",
"lib/netstandard2.1/BCrypt.Net-Next.dll",
"lib/netstandard2.1/BCrypt.Net-Next.xml",
"readme.md"
]
},
"Humanizer.Core/2.14.1": {
"sha512": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==",
"type": "package",
@@ -1244,6 +1419,21 @@
"logo.png"
]
},
"Microsoft.AspNetCore.Authentication.JwtBearer/8.0.11": {
"sha512": "9KhRuywosM24BPf1R5erwsvIkpRUu1+btVyOPlM3JgrhFVP4pq5Fuzi3vjP01OHXfbCtNhWa+HGkZeqaWdcO5w==",
"type": "package",
"path": "microsoft.aspnetcore.authentication.jwtbearer/8.0.11",
"files": [
".nupkg.metadata",
".signature.p7s",
"Icon.png",
"THIRD-PARTY-NOTICES.TXT",
"lib/net8.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll",
"lib/net8.0/Microsoft.AspNetCore.Authentication.JwtBearer.xml",
"microsoft.aspnetcore.authentication.jwtbearer.8.0.11.nupkg.sha512",
"microsoft.aspnetcore.authentication.jwtbearer.nuspec"
]
},
"Microsoft.AspNetCore.OpenApi/8.0.24": {
"sha512": "rqHY6POxy1e0vf7opG5hsxR0+Z0svcMYDvaEQW+T93/YeyFlaFOqQkZ6t1C8SaNLyH6LFlSnOXQ1Jf9Q+JFEhg==",
"type": "package",
@@ -2609,6 +2799,148 @@
"useSharedDesignerContext.txt"
]
},
"Microsoft.IdentityModel.Abstractions/8.2.1": {
"sha512": "8sMlmHhh5HdP3+yCSCUpJpN1yYrJ6J/V39df9siY8PeMckRMrSBRL/TMs/Jex6P1ly/Ie2mFqvhcPHHrNmCd/w==",
"type": "package",
"path": "microsoft.identitymodel.abstractions/8.2.1",
"files": [
".nupkg.metadata",
".signature.p7s",
"README.md",
"lib/net462/Microsoft.IdentityModel.Abstractions.dll",
"lib/net462/Microsoft.IdentityModel.Abstractions.xml",
"lib/net472/Microsoft.IdentityModel.Abstractions.dll",
"lib/net472/Microsoft.IdentityModel.Abstractions.xml",
"lib/net6.0/Microsoft.IdentityModel.Abstractions.dll",
"lib/net6.0/Microsoft.IdentityModel.Abstractions.xml",
"lib/net8.0/Microsoft.IdentityModel.Abstractions.dll",
"lib/net8.0/Microsoft.IdentityModel.Abstractions.xml",
"lib/net9.0/Microsoft.IdentityModel.Abstractions.dll",
"lib/net9.0/Microsoft.IdentityModel.Abstractions.xml",
"lib/netstandard2.0/Microsoft.IdentityModel.Abstractions.dll",
"lib/netstandard2.0/Microsoft.IdentityModel.Abstractions.xml",
"microsoft.identitymodel.abstractions.8.2.1.nupkg.sha512",
"microsoft.identitymodel.abstractions.nuspec"
]
},
"Microsoft.IdentityModel.JsonWebTokens/8.2.1": {
"sha512": "Oo0SBOzK6p3YIUcc1YTJCaYezVUa5HyUJ/AAB35QwxhhD6Blei5tNjNYDR0IbqHdb5EPUIiKcIbQGoj2b1mIbg==",
"type": "package",
"path": "microsoft.identitymodel.jsonwebtokens/8.2.1",
"files": [
".nupkg.metadata",
".signature.p7s",
"README.md",
"lib/net462/Microsoft.IdentityModel.JsonWebTokens.dll",
"lib/net462/Microsoft.IdentityModel.JsonWebTokens.xml",
"lib/net472/Microsoft.IdentityModel.JsonWebTokens.dll",
"lib/net472/Microsoft.IdentityModel.JsonWebTokens.xml",
"lib/net6.0/Microsoft.IdentityModel.JsonWebTokens.dll",
"lib/net6.0/Microsoft.IdentityModel.JsonWebTokens.xml",
"lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.dll",
"lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.xml",
"lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll",
"lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.xml",
"lib/netstandard2.0/Microsoft.IdentityModel.JsonWebTokens.dll",
"lib/netstandard2.0/Microsoft.IdentityModel.JsonWebTokens.xml",
"microsoft.identitymodel.jsonwebtokens.8.2.1.nupkg.sha512",
"microsoft.identitymodel.jsonwebtokens.nuspec"
]
},
"Microsoft.IdentityModel.Logging/8.2.1": {
"sha512": "EgSEAtBoWBynACdhKnMlVAFGGWqOIdmbpW7Vvx2SQ7u7ogZ50NcEGSoGljEsQoGIRYpo0UxXYktKcYMp+G/Bcg==",
"type": "package",
"path": "microsoft.identitymodel.logging/8.2.1",
"files": [
".nupkg.metadata",
".signature.p7s",
"README.md",
"lib/net462/Microsoft.IdentityModel.Logging.dll",
"lib/net462/Microsoft.IdentityModel.Logging.xml",
"lib/net472/Microsoft.IdentityModel.Logging.dll",
"lib/net472/Microsoft.IdentityModel.Logging.xml",
"lib/net6.0/Microsoft.IdentityModel.Logging.dll",
"lib/net6.0/Microsoft.IdentityModel.Logging.xml",
"lib/net8.0/Microsoft.IdentityModel.Logging.dll",
"lib/net8.0/Microsoft.IdentityModel.Logging.xml",
"lib/net9.0/Microsoft.IdentityModel.Logging.dll",
"lib/net9.0/Microsoft.IdentityModel.Logging.xml",
"lib/netstandard2.0/Microsoft.IdentityModel.Logging.dll",
"lib/netstandard2.0/Microsoft.IdentityModel.Logging.xml",
"microsoft.identitymodel.logging.8.2.1.nupkg.sha512",
"microsoft.identitymodel.logging.nuspec"
]
},
"Microsoft.IdentityModel.Protocols/7.1.2": {
"sha512": "SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==",
"type": "package",
"path": "microsoft.identitymodel.protocols/7.1.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"lib/net461/Microsoft.IdentityModel.Protocols.dll",
"lib/net461/Microsoft.IdentityModel.Protocols.xml",
"lib/net462/Microsoft.IdentityModel.Protocols.dll",
"lib/net462/Microsoft.IdentityModel.Protocols.xml",
"lib/net472/Microsoft.IdentityModel.Protocols.dll",
"lib/net472/Microsoft.IdentityModel.Protocols.xml",
"lib/net6.0/Microsoft.IdentityModel.Protocols.dll",
"lib/net6.0/Microsoft.IdentityModel.Protocols.xml",
"lib/net8.0/Microsoft.IdentityModel.Protocols.dll",
"lib/net8.0/Microsoft.IdentityModel.Protocols.xml",
"lib/netstandard2.0/Microsoft.IdentityModel.Protocols.dll",
"lib/netstandard2.0/Microsoft.IdentityModel.Protocols.xml",
"microsoft.identitymodel.protocols.7.1.2.nupkg.sha512",
"microsoft.identitymodel.protocols.nuspec"
]
},
"Microsoft.IdentityModel.Protocols.OpenIdConnect/7.1.2": {
"sha512": "6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==",
"type": "package",
"path": "microsoft.identitymodel.protocols.openidconnect/7.1.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"lib/net461/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll",
"lib/net461/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml",
"lib/net462/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll",
"lib/net462/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml",
"lib/net472/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll",
"lib/net472/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml",
"lib/net6.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll",
"lib/net6.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml",
"lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll",
"lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml",
"lib/netstandard2.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll",
"lib/netstandard2.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml",
"microsoft.identitymodel.protocols.openidconnect.7.1.2.nupkg.sha512",
"microsoft.identitymodel.protocols.openidconnect.nuspec"
]
},
"Microsoft.IdentityModel.Tokens/8.2.1": {
"sha512": "oQeLWCATuVXOCdIvouM4GG2xl1YNng+uAxYwu7CG6RuW+y+1+slXrOBq5csTU2pnV2SH3B1GmugDf6Jv/lexjw==",
"type": "package",
"path": "microsoft.identitymodel.tokens/8.2.1",
"files": [
".nupkg.metadata",
".signature.p7s",
"README.md",
"lib/net462/Microsoft.IdentityModel.Tokens.dll",
"lib/net462/Microsoft.IdentityModel.Tokens.xml",
"lib/net472/Microsoft.IdentityModel.Tokens.dll",
"lib/net472/Microsoft.IdentityModel.Tokens.xml",
"lib/net6.0/Microsoft.IdentityModel.Tokens.dll",
"lib/net6.0/Microsoft.IdentityModel.Tokens.xml",
"lib/net8.0/Microsoft.IdentityModel.Tokens.dll",
"lib/net8.0/Microsoft.IdentityModel.Tokens.xml",
"lib/net9.0/Microsoft.IdentityModel.Tokens.dll",
"lib/net9.0/Microsoft.IdentityModel.Tokens.xml",
"lib/netstandard2.0/Microsoft.IdentityModel.Tokens.dll",
"lib/netstandard2.0/Microsoft.IdentityModel.Tokens.xml",
"microsoft.identitymodel.tokens.8.2.1.nupkg.sha512",
"microsoft.identitymodel.tokens.nuspec"
]
},
"Microsoft.OpenApi/1.6.14": {
"sha512": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==",
"type": "package",
@@ -3032,6 +3364,30 @@
"useSharedDesignerContext.txt"
]
},
"System.IdentityModel.Tokens.Jwt/8.2.1": {
"sha512": "GVQmbjr2N8awFWPTWyThLxgKnFINObG1P+oX7vFrBY8um3V7V7Dh3wnxaGxNH6v6lSTeVQrY+SaUUBX9H3TPcw==",
"type": "package",
"path": "system.identitymodel.tokens.jwt/8.2.1",
"files": [
".nupkg.metadata",
".signature.p7s",
"README.md",
"lib/net462/System.IdentityModel.Tokens.Jwt.dll",
"lib/net462/System.IdentityModel.Tokens.Jwt.xml",
"lib/net472/System.IdentityModel.Tokens.Jwt.dll",
"lib/net472/System.IdentityModel.Tokens.Jwt.xml",
"lib/net6.0/System.IdentityModel.Tokens.Jwt.dll",
"lib/net6.0/System.IdentityModel.Tokens.Jwt.xml",
"lib/net8.0/System.IdentityModel.Tokens.Jwt.dll",
"lib/net8.0/System.IdentityModel.Tokens.Jwt.xml",
"lib/net9.0/System.IdentityModel.Tokens.Jwt.dll",
"lib/net9.0/System.IdentityModel.Tokens.Jwt.xml",
"lib/netstandard2.0/System.IdentityModel.Tokens.Jwt.dll",
"lib/netstandard2.0/System.IdentityModel.Tokens.Jwt.xml",
"system.identitymodel.tokens.jwt.8.2.1.nupkg.sha512",
"system.identitymodel.tokens.jwt.nuspec"
]
},
"System.IO.Pipelines/6.0.3": {
"sha512": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==",
"type": "package",
@@ -3195,12 +3551,15 @@
},
"projectFileDependencyGroups": {
"net8.0": [
"BCrypt.Net-Next >= 4.0.3",
"Microsoft.AspNetCore.Authentication.JwtBearer >= 8.0.11",
"Microsoft.AspNetCore.OpenApi >= 8.0.24",
"Microsoft.EntityFrameworkCore >= 8.0.11",
"Microsoft.EntityFrameworkCore.Design >= 8.0.11",
"Microsoft.EntityFrameworkCore.Sqlite >= 8.0.11",
"Microsoft.Extensions.Http >= 10.0.3",
"Swashbuckle.AspNetCore >= 6.6.2"
"Swashbuckle.AspNetCore >= 6.6.2",
"System.IdentityModel.Tokens.Jwt >= 8.2.1"
]
},
"packageFolders": {
@@ -3254,6 +3613,14 @@
"net8.0": {
"targetAlias": "net8.0",
"dependencies": {
"BCrypt.Net-Next": {
"target": "Package",
"version": "[4.0.3, )"
},
"Microsoft.AspNetCore.Authentication.JwtBearer": {
"target": "Package",
"version": "[8.0.11, )"
},
"Microsoft.AspNetCore.OpenApi": {
"target": "Package",
"version": "[8.0.24, )"
@@ -3279,6 +3646,10 @@
"Swashbuckle.AspNetCore": {
"target": "Package",
"version": "[6.6.2, )"
},
"System.IdentityModel.Tokens.Jwt": {
"target": "Package",
"version": "[8.2.1, )"
}
},
"imports": [

View File

@@ -1,10 +1,12 @@
{
"version": 2,
"dgSpecHash": "F/KzlzQBI38=",
"dgSpecHash": "6sDV5m7R2pw=",
"success": true,
"projectFilePath": "E:\\rr3\\RR3CommunityServer\\RR3CommunityServer\\RR3CommunityServer.csproj",
"expectedPackageFiles": [
"C:\\Users\\admin\\.nuget\\packages\\bcrypt.net-next\\4.0.3\\bcrypt.net-next.4.0.3.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\humanizer.core\\2.14.1\\humanizer.core.2.14.1.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.aspnetcore.authentication.jwtbearer\\8.0.11\\microsoft.aspnetcore.authentication.jwtbearer.8.0.11.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.aspnetcore.openapi\\8.0.24\\microsoft.aspnetcore.openapi.8.0.24.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.bcl.asyncinterfaces\\6.0.0\\microsoft.bcl.asyncinterfaces.6.0.0.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.codeanalysis.analyzers\\3.3.3\\microsoft.codeanalysis.analyzers.3.3.3.nupkg.sha512",
@@ -37,6 +39,12 @@
"C:\\Users\\admin\\.nuget\\packages\\microsoft.extensions.options\\10.0.3\\microsoft.extensions.options.10.0.3.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.extensions.options.configurationextensions\\10.0.3\\microsoft.extensions.options.configurationextensions.10.0.3.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.extensions.primitives\\10.0.3\\microsoft.extensions.primitives.10.0.3.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.identitymodel.abstractions\\8.2.1\\microsoft.identitymodel.abstractions.8.2.1.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.identitymodel.jsonwebtokens\\8.2.1\\microsoft.identitymodel.jsonwebtokens.8.2.1.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.identitymodel.logging\\8.2.1\\microsoft.identitymodel.logging.8.2.1.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.identitymodel.protocols\\7.1.2\\microsoft.identitymodel.protocols.7.1.2.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.identitymodel.protocols.openidconnect\\7.1.2\\microsoft.identitymodel.protocols.openidconnect.7.1.2.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.identitymodel.tokens\\8.2.1\\microsoft.identitymodel.tokens.8.2.1.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\microsoft.openapi\\1.6.14\\microsoft.openapi.1.6.14.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\mono.texttemplating\\2.2.1\\mono.texttemplating.2.2.1.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\sqlitepclraw.bundle_e_sqlite3\\2.1.6\\sqlitepclraw.bundle_e_sqlite3.2.1.6.nupkg.sha512",
@@ -56,6 +64,7 @@
"C:\\Users\\admin\\.nuget\\packages\\system.composition.runtime\\6.0.0\\system.composition.runtime.6.0.0.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\system.composition.typedparts\\6.0.0\\system.composition.typedparts.6.0.0.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\system.diagnostics.diagnosticsource\\10.0.3\\system.diagnostics.diagnosticsource.10.0.3.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\system.identitymodel.tokens.jwt\\8.2.1\\system.identitymodel.tokens.jwt.8.2.1.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\system.io.pipelines\\6.0.3\\system.io.pipelines.6.0.3.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\system.memory\\4.5.3\\system.memory.4.5.3.nupkg.sha512",
"C:\\Users\\admin\\.nuget\\packages\\system.reflection.metadata\\6.0.1\\system.reflection.metadata.6.0.1.nupkg.sha512",

Binary file not shown.

17
start-server.bat Normal file
View File

@@ -0,0 +1,17 @@
@echo off
echo ========================================
echo Starting RR3 Community Server
echo ========================================
echo.
echo Server will start on: http://localhost:5555
echo.
echo Keep this window open while using the panel!
echo Press Ctrl+C to stop the server
echo.
echo ========================================
echo.
cd /d E:\rr3\RR3CommunityServer\RR3CommunityServer
dotnet run --urls "http://localhost:5555"
pause