Compare commits
8 Commits
v1.2.0-dev
...
dd2c23000f
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2c23000f | |||
| f289cdfce9 | |||
| 15e842ce85 | |||
| f1d0d43cb7 | |||
| 5d2c3bf880 | |||
| e03c1d9856 | |||
| a6bab92282 | |||
| 8ba7c605f1 |
154
ASSET-MANIFEST-SPECIFICATION.md
Normal file
154
ASSET-MANIFEST-SPECIFICATION.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# RR3 Asset Manifest Specification
|
||||
|
||||
## Overview
|
||||
When uploading ZIP files to the RR3 Community Server, you can include a `manifest.json` or `manifest.xml` file to automatically configure asset metadata, version, and categorization.
|
||||
|
||||
## File Format Options
|
||||
|
||||
### Option 1: JSON Format (Recommended)
|
||||
Place `manifest.json` in the root of your ZIP file:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "9.3.0",
|
||||
"gameVersion": "9.3.0",
|
||||
"description": "Porsche Pack - 911 Models",
|
||||
"author": "CommunityModder",
|
||||
"category": "cars",
|
||||
"assets": [
|
||||
{
|
||||
"file": "porsche/911_turbo.dat",
|
||||
"category": "cars/porsche",
|
||||
"type": "Model",
|
||||
"required": true,
|
||||
"description": "Porsche 911 Turbo model"
|
||||
},
|
||||
{
|
||||
"file": "textures/911_turbo_paint.tex",
|
||||
"category": "textures/cars",
|
||||
"type": "Texture",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: XML Format
|
||||
Place `manifest.xml` in the root of your ZIP file:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<AssetManifest>
|
||||
<Version>9.3.0</Version>
|
||||
<GameVersion>9.3.0</GameVersion>
|
||||
<Description>Porsche Pack - 911 Models</Description>
|
||||
<Author>CommunityModder</Author>
|
||||
<Category>cars</Category>
|
||||
<Assets>
|
||||
<Asset>
|
||||
<File>porsche/911_turbo.dat</File>
|
||||
<Category>cars/porsche</Category>
|
||||
<Type>Model</Type>
|
||||
<Required>true</Required>
|
||||
<Description>Porsche 911 Turbo model</Description>
|
||||
</Asset>
|
||||
<Asset>
|
||||
<File>textures/911_turbo_paint.tex</File>
|
||||
<Category>textures/cars</Category>
|
||||
<Type>Texture</Type>
|
||||
<Required>false</Required>
|
||||
</Asset>
|
||||
</Assets>
|
||||
</AssetManifest>
|
||||
```
|
||||
|
||||
## Field Descriptions
|
||||
|
||||
### Root Fields
|
||||
- **version**: Asset pack version (e.g., "1.0.0")
|
||||
- **gameVersion**: RR3 game version this pack is for (e.g., "9.3.0")
|
||||
- **description**: Brief description of the asset pack
|
||||
- **author**: Creator name (optional)
|
||||
- **category**: Default category if not specified per-asset
|
||||
|
||||
### Asset Fields
|
||||
- **file**: Relative path to file within ZIP (required)
|
||||
- **category**: Asset category (overrides root category)
|
||||
- **type**: Asset type (Data, Texture, Audio, Model, Config)
|
||||
- **required**: Whether clients must download this (true/false)
|
||||
- **description**: Brief description (optional)
|
||||
|
||||
## Game Version Format
|
||||
- Use semantic versioning: `MAJOR.MINOR.PATCH`
|
||||
- Examples: `9.3.0`, `9.3.1`, `10.0.0`
|
||||
- **Compatibility**: Patch versions are compatible (9.3.x works with 9.3.0)
|
||||
|
||||
## Categories
|
||||
Standard categories:
|
||||
- `base` - Core game files
|
||||
- `cars` - Car models and data
|
||||
- `tracks` - Track models and data
|
||||
- `audio` - Sound effects and music
|
||||
- `textures` - Texture files
|
||||
- `ui` - User interface elements
|
||||
- `events` - Event configurations
|
||||
- `dlc` - Downloadable content
|
||||
- `updates` - Game updates
|
||||
|
||||
Subcategories allowed (e.g., `cars/porsche`, `tracks/silverstone`)
|
||||
|
||||
## Asset Types
|
||||
- `Data` - Generic data files (.dat, .pak, .z)
|
||||
- `Texture` - Texture files (.tex, .dds, .png)
|
||||
- `Audio` - Audio files (.ogg, .mp3, .wav)
|
||||
- `Model` - 3D model files (.nct, .obj)
|
||||
- `Config` - Configuration files (.json, .xml)
|
||||
|
||||
## Automatic Detection Fallback
|
||||
If no manifest file is provided, the server uses smart detection:
|
||||
1. Searches folder names for keywords (cars, tracks, audio, etc.)
|
||||
2. Preserves folder structure as subcategories
|
||||
3. Falls back to first folder name if no keywords match
|
||||
4. Version defaults to manual selection or "unknown"
|
||||
|
||||
## Example ZIP Structure
|
||||
|
||||
```
|
||||
my-asset-pack.zip
|
||||
├── manifest.json # Metadata file
|
||||
├── cars/
|
||||
│ ├── porsche/
|
||||
│ │ ├── 911_turbo.dat
|
||||
│ │ └── 911_gt3.dat
|
||||
│ └── ferrari/
|
||||
│ └── 488_gtb.dat
|
||||
└── textures/
|
||||
└── cars/
|
||||
└── paint_textures.pak
|
||||
```
|
||||
|
||||
## Upload Behavior
|
||||
1. Server extracts ZIP to temp location
|
||||
2. Searches for `manifest.json` or `manifest.xml` in root
|
||||
3. If found: Uses metadata from manifest
|
||||
4. If not found: Uses smart folder detection
|
||||
5. Processes each file according to configuration
|
||||
6. Calculates MD5/SHA256 hashes automatically
|
||||
7. Stores in database with version + category info
|
||||
|
||||
## Version Compatibility
|
||||
When a game client requests assets:
|
||||
- Client sends version: `9.3.1`
|
||||
- Server returns assets for: `9.3.0`, `9.3.1` (patch-compatible)
|
||||
- Server excludes: `9.2.x`, `9.4.x`, `10.x.x`
|
||||
|
||||
Major/minor versions must match exactly, patch versions are compatible within the same minor version.
|
||||
|
||||
## Best Practices
|
||||
1. Always include a manifest file for large packs
|
||||
2. Use semantic versioning for both pack and game version
|
||||
3. Mark base game assets as `required: true`
|
||||
4. Use descriptive categories and subcategories
|
||||
5. Include author information for community tracking
|
||||
6. Test with single-file upload before bulk ZIP upload
|
||||
7. Use JSON format for better tool support
|
||||
128
RR3CommunityServer/Controllers/AuthController.cs
Normal file
128
RR3CommunityServer/Controllers/AuthController.cs
Normal 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" });
|
||||
}
|
||||
}
|
||||
174
RR3CommunityServer/Controllers/ServerSettingsController.cs
Normal file
174
RR3CommunityServer/Controllers/ServerSettingsController.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using RR3CommunityServer.Models;
|
||||
|
||||
namespace RR3CommunityServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/settings")]
|
||||
public class ServerSettingsController : ControllerBase
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
private readonly ILogger<ServerSettingsController> _logger;
|
||||
|
||||
public ServerSettingsController(RR3DbContext context, ILogger<ServerSettingsController> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user settings (called by APK sync button)
|
||||
/// GET /api/settings/getUserSettings?deviceId=xxx
|
||||
/// </summary>
|
||||
[HttpGet("getUserSettings")]
|
||||
public async Task<ActionResult<UserSettingsResponse>> GetUserSettings([FromQuery] string? deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
_logger.LogWarning("GetUserSettings: No deviceId provided");
|
||||
return BadRequest(new { error = "deviceId is required" });
|
||||
}
|
||||
|
||||
_logger.LogInformation($"🔄 GetUserSettings: deviceId={deviceId}");
|
||||
|
||||
var settings = await _context.UserSettings
|
||||
.Where(s => s.DeviceId == deviceId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
_logger.LogInformation($"⚠️ No settings found for deviceId={deviceId}, returning defaults");
|
||||
return Ok(new UserSettingsResponse
|
||||
{
|
||||
mode = "offline",
|
||||
serverUrl = "",
|
||||
message = "No settings found, using defaults"
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation($"✅ Found settings: mode={settings.Mode}, url={settings.ServerUrl}");
|
||||
return Ok(new UserSettingsResponse
|
||||
{
|
||||
mode = settings.Mode,
|
||||
serverUrl = settings.ServerUrl,
|
||||
message = "Settings retrieved successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ Error in GetUserSettings");
|
||||
return StatusCode(500, new { error = "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update user settings (called by web panel)
|
||||
/// POST /api/settings/updateUserSettings
|
||||
/// Body: { "deviceId": "xxx", "mode": "online", "serverUrl": "https://example.com:8443" }
|
||||
/// </summary>
|
||||
[HttpPost("updateUserSettings")]
|
||||
public async Task<ActionResult<UpdateSettingsResponse>> UpdateUserSettings([FromBody] UpdateUserSettingsRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.deviceId))
|
||||
{
|
||||
return BadRequest(new { error = "deviceId is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.mode))
|
||||
{
|
||||
return BadRequest(new { error = "mode is required" });
|
||||
}
|
||||
|
||||
_logger.LogInformation($"🔄 UpdateUserSettings: deviceId={request.deviceId}, mode={request.mode}, url={request.serverUrl}");
|
||||
|
||||
var settings = await _context.UserSettings
|
||||
.Where(s => s.DeviceId == request.deviceId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
// Create new settings
|
||||
settings = new UserSettings
|
||||
{
|
||||
DeviceId = request.deviceId,
|
||||
Mode = request.mode,
|
||||
ServerUrl = request.serverUrl ?? "",
|
||||
LastUpdated = DateTime.UtcNow
|
||||
};
|
||||
_context.UserSettings.Add(settings);
|
||||
_logger.LogInformation($"➕ Created new settings for deviceId={request.deviceId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing settings
|
||||
settings.Mode = request.mode;
|
||||
settings.ServerUrl = request.serverUrl ?? "";
|
||||
settings.LastUpdated = DateTime.UtcNow;
|
||||
_logger.LogInformation($"✏️ Updated existing settings for deviceId={request.deviceId}");
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new UpdateSettingsResponse
|
||||
{
|
||||
success = true,
|
||||
message = "Settings updated successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ Error in UpdateUserSettings");
|
||||
return StatusCode(500, new { error = "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all user settings (for admin panel)
|
||||
/// GET /api/settings/getAllUserSettings
|
||||
/// </summary>
|
||||
[HttpGet("getAllUserSettings")]
|
||||
public async Task<ActionResult<List<UserSettings>>> GetAllUserSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allSettings = await _context.UserSettings
|
||||
.OrderByDescending(s => s.LastUpdated)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInformation($"📋 Retrieved {allSettings.Count} user settings");
|
||||
return Ok(allSettings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ Error in GetAllUserSettings");
|
||||
return StatusCode(500, new { error = "Internal server error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Response models
|
||||
public class UserSettingsResponse
|
||||
{
|
||||
public string mode { get; set; } = "offline";
|
||||
public string serverUrl { get; set; } = "";
|
||||
public string? message { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateUserSettingsRequest
|
||||
{
|
||||
public string deviceId { get; set; } = string.Empty;
|
||||
public string mode { get; set; } = "offline";
|
||||
public string? serverUrl { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateSettingsResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? message { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Models;
|
||||
|
||||
namespace RR3CommunityServer.Data;
|
||||
|
||||
@@ -9,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; }
|
||||
@@ -20,6 +23,7 @@ public class RR3DbContext : DbContext
|
||||
public DbSet<CareerProgress> CareerProgress { get; set; }
|
||||
public DbSet<GameAsset> GameAssets { get; set; }
|
||||
public DbSet<ModPack> ModPacks { get; set; }
|
||||
public DbSet<UserSettings> UserSettings { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -377,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; }
|
||||
@@ -394,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; }
|
||||
|
||||
856
RR3CommunityServer/Migrations/20260219180936_AddUserSettings.Designer.cs
generated
Normal file
856
RR3CommunityServer/Migrations/20260219180936_AddUserSettings.Designer.cs
generated
Normal file
@@ -0,0 +1,856 @@
|
||||
// <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("20260219180936_AddUserSettings")]
|
||||
partial class AddUserSettings
|
||||
{
|
||||
/// <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<DateTime>("DownloadedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EaCdnPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileSha256")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsAvailable")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCustomContent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastAccessedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LocalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Md5Hash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OriginalUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TrackId")
|
||||
.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, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3366),
|
||||
GoldReward = 50,
|
||||
Name = "Daily Sprint Challenge",
|
||||
StartDate = new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3363),
|
||||
TargetTime = 90.5,
|
||||
TrackName = "Silverstone National"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Active = true,
|
||||
CarName = "Any Car",
|
||||
CashReward = 25000,
|
||||
EndDate = new DateTime(2026, 2, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375),
|
||||
GoldReward = 100,
|
||||
Name = "Speed Demon Trial",
|
||||
StartDate = new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375),
|
||||
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.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.Data.User", b =>
|
||||
{
|
||||
b.Navigation("CareerProgress");
|
||||
|
||||
b.Navigation("OwnedCars");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RR3CommunityServer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserSettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DeviceId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ServerUrl = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Mode = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LastUpdated = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserSettings", x => x.Id);
|
||||
});
|
||||
|
||||
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) });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserSettings");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 25, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5137), new DateTime(2026, 2, 18, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5134) });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 25, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5146), new DateTime(2026, 2, 18, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5146) });
|
||||
}
|
||||
}
|
||||
}
|
||||
966
RR3CommunityServer/Migrations/20260219233025_UpdateGameAssetFields.Designer.cs
generated
Normal file
966
RR3CommunityServer/Migrations/20260219233025_UpdateGameAssetFields.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
966
RR3CommunityServer/Migrations/20260220175450_AddGameVersioning.Designer.cs
generated
Normal file
966
RR3CommunityServer/Migrations/20260220175450_AddGameVersioning.Designer.cs
generated
Normal 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("20260220175450_AddGameVersioning")]
|
||||
partial class AddGameVersioning
|
||||
{
|
||||
/// <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, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3387),
|
||||
GoldReward = 50,
|
||||
Name = "Daily Sprint Challenge",
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3384),
|
||||
TargetTime = 90.5,
|
||||
TrackName = "Silverstone National"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Active = true,
|
||||
CarName = "Any Car",
|
||||
CashReward = 25000,
|
||||
EndDate = new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
GoldReward = 100,
|
||||
Name = "Speed Demon Trial",
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RR3CommunityServer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGameVersioning : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3387), new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3384) });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395), new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395) });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, 25, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5137),
|
||||
EndDate = new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3387),
|
||||
GoldReward = 50,
|
||||
Name = "Daily Sprint Challenge",
|
||||
StartDate = new DateTime(2026, 2, 18, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5134),
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3384),
|
||||
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, 25, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5146),
|
||||
EndDate = new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
GoldReward = 100,
|
||||
Name = "Speed Demon Trial",
|
||||
StartDate = new DateTime(2026, 2, 18, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5146),
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
TargetTime = 120.0,
|
||||
TrackName = "Dubai Autodrome"
|
||||
});
|
||||
@@ -797,6 +804,110 @@ 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")
|
||||
.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)
|
||||
@@ -815,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
|
||||
}
|
||||
}
|
||||
|
||||
113
RR3CommunityServer/Models/AccountModels.cs
Normal file
113
RR3CommunityServer/Models/AccountModels.cs
Normal 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; }
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
namespace RR3CommunityServer.Models;
|
||||
|
||||
// User Settings for Server Configuration
|
||||
public class UserSettings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
public string ServerUrl { get; set; } = string.Empty;
|
||||
public string Mode { get; set; } = "offline"; // "online" or "offline"
|
||||
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Progression request/response models
|
||||
public class ProgressionUpdate
|
||||
{
|
||||
|
||||
@@ -109,6 +109,9 @@
|
||||
<a href="/admin/purchases" class="btn btn-warning">
|
||||
<i class="bi bi-cart"></i> View Purchases
|
||||
</a>
|
||||
<a href="/devicesettings" class="btn btn-primary">
|
||||
<i class="bi bi-phone"></i> Device Settings
|
||||
</a>
|
||||
<a href="/admin/settings" class="btn btn-secondary">
|
||||
<i class="bi bi-gear"></i> Server Settings
|
||||
</a>
|
||||
|
||||
@@ -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;
|
||||
|
||||
473
RR3CommunityServer/Pages/Assets.cshtml
Normal file
473
RR3CommunityServer/Pages/Assets.cshtml
Normal file
@@ -0,0 +1,473 @@
|
||||
@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">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="single-tab" data-bs-toggle="tab" data-bs-target="#single" type="button" role="tab">
|
||||
📄 Single File
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="zip-tab" data-bs-toggle="tab" data-bs-target="#zip" type="button" role="tab">
|
||||
📦 ZIP Upload
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="url-tab" data-bs-toggle="tab" data-bs-target="#url" type="button" role="tab">
|
||||
🌐 URL Download
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Single File Upload -->
|
||||
<div class="tab-pane fade show active" id="single" role="tabpanel">
|
||||
<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 for="gameVersion" class="form-label">Game Version</label>
|
||||
<select class="form-select" id="gameVersion" name="gameVersion" required>
|
||||
<option value="">Select version...</option>
|
||||
<option value="9.3.0">9.3.0</option>
|
||||
<option value="9.2.0">9.2.0</option>
|
||||
<option value="9.1.0">9.1.0</option>
|
||||
<option value="9.0.0">9.0.0</option>
|
||||
<option value="8.9.0">8.9.0</option>
|
||||
<option value="universal">Universal (All Versions)</option>
|
||||
</select>
|
||||
<small class="text-muted">Patch-compatible: 9.3.x works with 9.3.0</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Options</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
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Asset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ZIP Bulk Upload -->
|
||||
<div class="tab-pane fade" id="zip" role="tabpanel">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> <strong>ZIP Upload:</strong>
|
||||
Folder structure preserved • Auto MD5 calculation • Manifest.json support
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" asp-page-handler="UploadZip">
|
||||
<div class="mb-3">
|
||||
<label for="zipFile" class="form-label">ZIP Archive</label>
|
||||
<input class="form-control" type="file" id="zipFile" name="zipFile" accept=".zip" required>
|
||||
<small class="text-muted">Include manifest.json for auto-detection • Example: cars/porsche_911.dat → /cars/porsche_911.dat</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="zipGameVersion" class="form-label">Game Version</label>
|
||||
<select class="form-select" id="zipGameVersion" name="gameVersion" required>
|
||||
<option value="">Detect from manifest...</option>
|
||||
<option value="9.3.0">9.3.0 (Latest)</option>
|
||||
<option value="9.2.0">9.2.0</option>
|
||||
<option value="9.1.0">9.1.0</option>
|
||||
<option value="9.0.0">9.0.0</option>
|
||||
<option value="8.9.0">8.9.0</option>
|
||||
<option value="universal">Universal</option>
|
||||
</select>
|
||||
<small class="text-muted">Or specify in manifest.json</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="baseCategory" class="form-label">Base Category</label>
|
||||
<select class="form-select" id="baseCategory" name="baseCategory">
|
||||
<option value="auto">🤖 Auto-detect</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 class="form-label d-block">Options</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="isRequiredZip" name="isRequired" checked>
|
||||
<label class="form-check-label" for="isRequiredZip">
|
||||
Mark as required
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-file-zip"></i> Extract and Upload
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- URL Download Tab -->
|
||||
<div class="tab-pane fade" id="url" role="tabpanel">
|
||||
<div class="alert alert-success">
|
||||
<i class="bi bi-cloud-arrow-down"></i> <strong>Direct Download:</strong>
|
||||
Server downloads ZIP directly • No browser upload needed • Perfect for large files
|
||||
</div>
|
||||
<form method="post" asp-page-handler="DownloadZip">
|
||||
<div class="mb-3">
|
||||
<label for="zipUrl" class="form-label">ZIP File URL</label>
|
||||
<input type="url" class="form-control" id="zipUrl" name="zipUrl"
|
||||
placeholder="https://example.com/assets/rr3-cars-pack.zip" required>
|
||||
<small class="text-muted">Direct link to ZIP file (http:// or https://)</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label for="baseCategoryUrl" class="form-label">Base Category (optional)</label>
|
||||
<select class="form-select" id="baseCategoryUrl" name="baseCategory">
|
||||
<option value="base">Auto-Detect (Smart)</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>
|
||||
<small class="text-muted">System will auto-detect categories from folder names</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block"> </label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="isRequiredUrl" name="isRequired" checked>
|
||||
<label class="form-check-label" for="isRequiredUrl">
|
||||
All required
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-cloud-download"></i> Download and Extract
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</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]}";
|
||||
}
|
||||
}
|
||||
587
RR3CommunityServer/Pages/Assets.cshtml.cs
Normal file
587
RR3CommunityServer/Pages/Assets.cshtml.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
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;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
|
||||
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 gameVersion,
|
||||
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.Version = gameVersion;
|
||||
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),
|
||||
Version = gameVersion,
|
||||
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> OnPostUploadZipAsync(
|
||||
IFormFile zipFile,
|
||||
string baseCategory,
|
||||
string? gameVersion,
|
||||
bool isRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (zipFile == null || zipFile.Length == 0)
|
||||
{
|
||||
Message = "No ZIP file selected.";
|
||||
IsError = true;
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (!zipFile.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Message = "Please upload a ZIP file.";
|
||||
IsError = true;
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Ensure assets directory exists
|
||||
if (!Directory.Exists(_assetsBasePath))
|
||||
{
|
||||
Directory.CreateDirectory(_assetsBasePath);
|
||||
}
|
||||
|
||||
// Save ZIP to temp location
|
||||
var tempZipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
|
||||
using (var stream = new FileStream(tempZipPath, FileMode.Create))
|
||||
{
|
||||
await zipFile.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
int extractedCount = 0;
|
||||
int skippedCount = 0;
|
||||
var errors = new List<string>();
|
||||
|
||||
// Try to parse manifest file if exists
|
||||
AssetManifest? manifest = null;
|
||||
using (var archive = ZipFile.OpenRead(tempZipPath))
|
||||
{
|
||||
var manifestEntry = archive.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
e.Name.Equals("manifest.xml", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (manifestEntry != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = manifestEntry.Open();
|
||||
using var reader = new StreamReader(stream);
|
||||
var manifestContent = await reader.ReadToEndAsync();
|
||||
|
||||
if (manifestEntry.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
manifest = JsonSerializer.Deserialize<AssetManifest>(manifestContent,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
|
||||
if (manifest != null)
|
||||
{
|
||||
_logger.LogInformation("Loaded manifest: Version={Version}, GameVersion={GameVersion}",
|
||||
manifest.Version, manifest.GameVersion);
|
||||
|
||||
// Override parameters from manifest if provided
|
||||
if (!string.IsNullOrEmpty(manifest.GameVersion))
|
||||
gameVersion = manifest.GameVersion;
|
||||
if (!string.IsNullOrEmpty(manifest.Category) && baseCategory == "auto")
|
||||
baseCategory = manifest.Category;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to parse manifest file, using defaults");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract ZIP and process each file
|
||||
using (var archive = ZipFile.OpenRead(tempZipPath))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Skip directories and manifest files
|
||||
if (string.IsNullOrEmpty(entry.Name) || entry.FullName.EndsWith("/") ||
|
||||
entry.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
entry.Name.Equals("manifest.xml", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// Check if this file has manifest metadata
|
||||
ManifestAsset? manifestAsset = manifest?.Assets?.FirstOrDefault(a =>
|
||||
a.File.Replace("\\", "/").Equals(entry.FullName.Replace("\\", "/"),
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Determine category from manifest or path in ZIP
|
||||
var pathParts = entry.FullName.Split('/', '\\');
|
||||
var category = manifestAsset?.Category ?? baseCategory;
|
||||
|
||||
// Auto-detect if category is "auto"
|
||||
if (category == "auto")
|
||||
{
|
||||
category = SmartDetectCategory(entry.FullName) ?? "base";
|
||||
}
|
||||
|
||||
// If ZIP has folders and no manifest, use first folder as subcategory
|
||||
if (manifestAsset == null && pathParts.Length > 1 && category != "auto")
|
||||
{
|
||||
category = Path.Combine(category, pathParts[0]);
|
||||
}
|
||||
|
||||
// Create category subdirectory
|
||||
var categoryPath = Path.Combine(_assetsBasePath, category);
|
||||
if (!Directory.Exists(categoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(categoryPath);
|
||||
}
|
||||
|
||||
// Extract file
|
||||
var fileName = entry.Name;
|
||||
var localPath = Path.Combine(categoryPath, fileName);
|
||||
|
||||
// Extract to disk
|
||||
entry.ExtractToFile(localPath, overwrite: true);
|
||||
|
||||
// Calculate hashes
|
||||
var md5Hash = await CalculateMd5Async(localPath);
|
||||
var sha256Hash = await CalculateSha256Async(localPath);
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
|
||||
// Build EA CDN path from ZIP structure
|
||||
var eaCdnPath = "/" + entry.FullName.Replace("\\", "/");
|
||||
|
||||
// Determine asset type from manifest or filename
|
||||
var assetType = manifestAsset?.Type ?? DetermineAssetType(fileName);
|
||||
var required = manifestAsset?.Required ?? isRequired;
|
||||
var description = manifestAsset?.Description;
|
||||
|
||||
// Check if asset already exists
|
||||
var existingAsset = await _context.GameAssets
|
||||
.FirstOrDefaultAsync(a => a.EaCdnPath == eaCdnPath);
|
||||
|
||||
if (existingAsset != null)
|
||||
{
|
||||
// Update existing
|
||||
existingAsset.FileName = fileName;
|
||||
existingAsset.LocalPath = localPath;
|
||||
existingAsset.FileSize = fileInfo.Length;
|
||||
existingAsset.Md5Hash = md5Hash;
|
||||
existingAsset.FileSha256 = sha256Hash;
|
||||
existingAsset.Category = category;
|
||||
existingAsset.AssetType = assetType;
|
||||
existingAsset.IsRequired = required;
|
||||
existingAsset.Description = description;
|
||||
existingAsset.ContentType = GetContentType(fileName);
|
||||
existingAsset.Version = gameVersion;
|
||||
existingAsset.UploadedAt = DateTime.UtcNow;
|
||||
skippedCount++;
|
||||
}
|
||||
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 = required,
|
||||
Description = description ?? $"Extracted from {zipFile.FileName}",
|
||||
ContentType = GetContentType(fileName),
|
||||
Version = gameVersion,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
DownloadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.GameAssets.Add(asset);
|
||||
extractedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"{entry.FullName}: {ex.Message}");
|
||||
_logger.LogError(ex, "Error extracting file from ZIP: {FileName}", entry.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save changes
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Clean up temp ZIP
|
||||
if (System.IO.File.Exists(tempZipPath))
|
||||
{
|
||||
System.IO.File.Delete(tempZipPath);
|
||||
}
|
||||
|
||||
// Build message
|
||||
if (errors.Any())
|
||||
{
|
||||
Message = $"ZIP processed: {extractedCount} new, {skippedCount} updated. Errors: {errors.Count}";
|
||||
IsError = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Message = $"ZIP extracted successfully! {extractedCount} new files, {skippedCount} updated.";
|
||||
}
|
||||
|
||||
_logger.LogInformation("ZIP uploaded: {FileName} -> {ExtractedCount} files", zipFile.FileName, extractedCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing ZIP file");
|
||||
Message = $"Error processing ZIP: {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"
|
||||
};
|
||||
}
|
||||
|
||||
private string? SmartDetectCategory(string fullPath)
|
||||
{
|
||||
var lowerPath = fullPath.ToLowerInvariant();
|
||||
|
||||
// Check for known keywords in path
|
||||
if (lowerPath.Contains("car") || lowerPath.Contains("vehicle") || lowerPath.Contains("automobile"))
|
||||
return "cars";
|
||||
if (lowerPath.Contains("track") || lowerPath.Contains("circuit") || lowerPath.Contains("course"))
|
||||
return "tracks";
|
||||
if (lowerPath.Contains("audio") || lowerPath.Contains("sound") || lowerPath.Contains("music"))
|
||||
return "audio";
|
||||
if (lowerPath.Contains("texture") || lowerPath.Contains("material") || lowerPath.Contains("skin"))
|
||||
return "textures";
|
||||
if (lowerPath.Contains("ui") || lowerPath.Contains("hud") || lowerPath.Contains("menu"))
|
||||
return "ui";
|
||||
if (lowerPath.Contains("event") || lowerPath.Contains("race") || lowerPath.Contains("challenge"))
|
||||
return "events";
|
||||
if (lowerPath.Contains("dlc") || lowerPath.Contains("expansion") || lowerPath.Contains("addon"))
|
||||
return "dlc";
|
||||
if (lowerPath.Contains("update") || lowerPath.Contains("patch"))
|
||||
return "updates";
|
||||
|
||||
// Fall back to first folder name
|
||||
var pathParts = fullPath.Split('/', '\\');
|
||||
if (pathParts.Length > 1)
|
||||
return pathParts[0].ToLowerInvariant();
|
||||
|
||||
return null; // Will default to "base"
|
||||
}
|
||||
|
||||
private string DetermineAssetType(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return extension switch
|
||||
{
|
||||
".png" or ".jpg" or ".jpeg" or ".pvr" or ".atlas" => "Texture",
|
||||
".mp3" or ".ogg" or ".wav" => "Audio",
|
||||
".json" or ".xml" => "Config",
|
||||
".pak" or ".dat" or ".nct" => "Data",
|
||||
_ => "Data"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class AssetStats
|
||||
{
|
||||
public int TotalAssets { get; set; }
|
||||
public int AvailableAssets { get; set; }
|
||||
public long TotalSizeMB { get; set; }
|
||||
public int TotalDownloads { get; set; }
|
||||
}
|
||||
|
||||
// Manifest models for automatic ZIP metadata detection
|
||||
public class AssetManifest
|
||||
{
|
||||
public string? Version { get; set; }
|
||||
public string? GameVersion { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Author { get; set; }
|
||||
public string? Category { get; set; }
|
||||
public List<ManifestAsset>? Assets { get; set; }
|
||||
}
|
||||
|
||||
public class ManifestAsset
|
||||
{
|
||||
public string File { get; set; } = string.Empty;
|
||||
public string? Category { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public bool Required { get; set; } = true;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
201
RR3CommunityServer/Pages/DeviceSettings.cshtml
Normal file
201
RR3CommunityServer/Pages/DeviceSettings.cshtml
Normal file
@@ -0,0 +1,201 @@
|
||||
@page
|
||||
@model RR3CommunityServer.Pages.DeviceSettingsModel
|
||||
@{
|
||||
ViewData["Title"] = "Device Server Settings";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1>📱 Device Server Settings</h1>
|
||||
<p class="text-muted">Configure server URLs for individual devices (syncs with APK)</p>
|
||||
</div>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["Message"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<strong>✅ Success!</strong> @TempData["Message"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Add New Device Settings -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">➕ Add New Device Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" asp-page-handler="AddOrUpdate">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="deviceId" class="form-label">Device ID</label>
|
||||
<input type="text" class="form-control" id="deviceId" name="deviceId"
|
||||
placeholder="e.g., device_abc123" required>
|
||||
<small class="text-muted">Enter the device ID from the APK</small>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="mode" class="form-label">Mode</label>
|
||||
<select class="form-select" id="mode" name="mode" required>
|
||||
<option value="offline">📱 Offline</option>
|
||||
<option value="online" selected>🌐 Online</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5 mb-3">
|
||||
<label for="serverUrl" class="form-label">Server URL</label>
|
||||
<input type="text" class="form-control" id="serverUrl" name="serverUrl"
|
||||
placeholder="https://example.com:8443" value="@Model.CurrentServerUrl">
|
||||
<small class="text-muted">Include port if not 80/443</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle"></i> Add / Update Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="mb-3">ℹ️ How It Works</h6>
|
||||
<ol class="small mb-0">
|
||||
<li>Add device configuration here</li>
|
||||
<li>User opens RR3 APK</li>
|
||||
<li>User taps "🔄 Sync from Web Panel"</li>
|
||||
<li>APK fetches settings from this server</li>
|
||||
<li>Game restarts with new settings</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing Device Settings -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">🗂️ Configured Devices (@Model.DeviceSettings.Count)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (Model.DeviceSettings.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No device settings configured yet. Add one above to get started.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Device ID</th>
|
||||
<th>Mode</th>
|
||||
<th>Server URL</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var setting in Model.DeviceSettings)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@setting.DeviceId</code></td>
|
||||
<td>
|
||||
@if (setting.Mode == "online")
|
||||
{
|
||||
<span class="badge bg-success">🌐 Online</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">📱 Offline</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(setting.ServerUrl))
|
||||
{
|
||||
<code>@setting.ServerUrl</code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
@setting.LastUpdated.ToLocalTime().ToString("MMM dd, yyyy HH:mm")
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" asp-page-handler="Delete" class="d-inline">
|
||||
<input type="hidden" name="deviceId" value="@setting.DeviceId" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Delete settings for @setting.DeviceId?')">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Documentation -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">📚 API Endpoints</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>GET /api/settings/getUserSettings?deviceId={deviceId}</h6>
|
||||
<p class="text-muted">Returns server configuration for a device (called by APK sync button)</p>
|
||||
<pre class="bg-light p-3"><code>{
|
||||
"mode": "online",
|
||||
"serverUrl": "https://rr3.example.com:8443",
|
||||
"message": "Settings retrieved successfully"
|
||||
}</code></pre>
|
||||
|
||||
<h6 class="mt-4">POST /api/settings/updateUserSettings</h6>
|
||||
<p class="text-muted">Update settings from web panel (this page uses it)</p>
|
||||
<pre class="bg-light p-3"><code>{
|
||||
"deviceId": "device_abc123",
|
||||
"mode": "online",
|
||||
"serverUrl": "https://rr3.example.com:8443"
|
||||
}</code></pre>
|
||||
|
||||
<h6 class="mt-4">GET /api/settings/getAllUserSettings</h6>
|
||||
<p class="text-muted">Get all device settings (admin only)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Auto-populate server URL field with current server
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const serverUrlInput = document.getElementById('serverUrl');
|
||||
if (!serverUrlInput.value) {
|
||||
serverUrlInput.value = '@Model.CurrentServerUrl';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
110
RR3CommunityServer/Pages/DeviceSettings.cshtml.cs
Normal file
110
RR3CommunityServer/Pages/DeviceSettings.cshtml.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
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;
|
||||
private readonly ILogger<DeviceSettingsModel> _logger;
|
||||
|
||||
public DeviceSettingsModel(RR3DbContext context, ILogger<DeviceSettingsModel> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<UserSettings> DeviceSettings { get; set; } = new();
|
||||
public string CurrentServerUrl { get; set; } = string.Empty;
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
CurrentServerUrl = $"{Request.Scheme}://{Request.Host}";
|
||||
DeviceSettings = await _context.UserSettings
|
||||
.OrderByDescending(s => s.LastUpdated)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInformation($"📋 Loaded {DeviceSettings.Count} device settings");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAddOrUpdateAsync(string deviceId, string mode, string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
TempData["Error"] = "Device ID is required";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
_logger.LogInformation($"🔄 Adding/Updating settings: deviceId={deviceId}, mode={mode}, url={serverUrl}");
|
||||
|
||||
var existingSettings = await _context.UserSettings
|
||||
.Where(s => s.DeviceId == deviceId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingSettings == null)
|
||||
{
|
||||
// Create new
|
||||
var newSettings = new UserSettings
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
Mode = mode,
|
||||
ServerUrl = serverUrl ?? string.Empty,
|
||||
LastUpdated = DateTime.UtcNow
|
||||
};
|
||||
_context.UserSettings.Add(newSettings);
|
||||
_logger.LogInformation($"➕ Created new settings for {deviceId}");
|
||||
TempData["Message"] = $"Settings created for device: {deviceId}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing
|
||||
existingSettings.Mode = mode;
|
||||
existingSettings.ServerUrl = serverUrl ?? string.Empty;
|
||||
existingSettings.LastUpdated = DateTime.UtcNow;
|
||||
_logger.LogInformation($"✏️ Updated settings for {deviceId}");
|
||||
TempData["Message"] = $"Settings updated for device: {deviceId}";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ Error saving device settings");
|
||||
TempData["Error"] = "Failed to save settings";
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = await _context.UserSettings
|
||||
.Where(s => s.DeviceId == deviceId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (settings != null)
|
||||
{
|
||||
_context.UserSettings.Remove(settings);
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInformation($"🗑️ Deleted settings for {deviceId}");
|
||||
TempData["Message"] = $"Settings deleted for device: {deviceId}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ Error deleting device settings");
|
||||
TempData["Error"] = "Failed to delete settings";
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
164
RR3CommunityServer/Pages/Login.cshtml
Normal file
164
RR3CommunityServer/Pages/Login.cshtml
Normal 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>
|
||||
86
RR3CommunityServer/Pages/Login.cshtml.cs
Normal file
86
RR3CommunityServer/Pages/Login.cshtml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
27
RR3CommunityServer/Pages/Logout.cshtml.cs
Normal file
27
RR3CommunityServer/Pages/Logout.cshtml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
209
RR3CommunityServer/Pages/Register.cshtml
Normal file
209
RR3CommunityServer/Pages/Register.cshtml
Normal 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>
|
||||
110
RR3CommunityServer/Pages/Register.cshtml.cs
Normal file
110
RR3CommunityServer/Pages/Register.cshtml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,8 +35,12 @@ 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>();
|
||||
|
||||
// Add HttpClient for URL downloads
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// CORS for cross-origin requests
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -52,16 +71,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 ║");
|
||||
|
||||
@@ -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>
|
||||
|
||||
380
RR3CommunityServer/Services/AuthService.cs
Normal file
380
RR3CommunityServer/Services/AuthService.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
BIN
RR3CommunityServer/bin/Debug/net8.0/BCrypt.Net-Next.dll
Normal file
BIN
RR3CommunityServer/bin/Debug/net8.0/BCrypt.Net-Next.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
BIN
RR3CommunityServer/bin/Debug/net8.0/rr3community.db
Normal file
BIN
RR3CommunityServer/bin/Debug/net8.0/rr3community.db
Normal file
Binary file not shown.
@@ -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+f289cdfce9c28a229d8a00547a730c54530d68e9")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("RR3CommunityServer")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("RR3CommunityServer")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
a9e857267e797d27d55007236bf2e0f3befeb9ad1a31a95f91c42d4df2f35dc7
|
||||
0da7d7611f502a833d49c872526fd4633f406323449f2304a3325dd643f82480
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
ef2a9acb1383590916d3f16acb1a638605828a26b186fcd899f4f16268addae3
|
||||
710d43ac46614dc8b6a0b200c2dda2aea7d3365426e586abb3f00369358fbe59
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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/f289cdfce9c28a229d8a00547a730c54530d68e9/*"}}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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=","S5l3\u002BBR9dKGtXWyJK2BwVbV5FvygplG8\u002Byh9AE1ZYRQ=","hnhSRoeFpk3C6XWICUlX/lNip6TfbZWFYZv4weSCyrw=","fVR30KYkDSf6Wvsw9TujzlqruhwIMbw1wHxa1z/mksA=","bGtvAdvcs6Zz1qOTjdKz5gd/5jOpXDLvMjTZye3i/QI=","EoVh8vBcGohUnEMEoZuTXrpZ9uBDHT19VmDHc/D\u002Bm0I=","7gMXO5\u002Bhli7od21x4gC/qf3G6ddyyMyoSF6YFX9IaKg=","IdEjAFCVk3xZYjiEMESONot/jkvTj/gnwS5nnpGaIMc=","JVRe\u002Be2d47FunIfxVYRpqRFtljZ8gqrK3xMRy6TCd\u002BQ=","DQG0T8n9f5ohwv9akihU55D4/3WR7\u002BlDnvkdsAHHSgc=","VxDQNRQXYUU41o9SG4HrkKWR59FJIv8lmnwBolB/wE0=","x88k5Bg2fv\u002Bie1eIqFd4doOTQY0lwCNPv/5eJfhIK\u002Bw=","0Slg2/xnc5E9nXprYyph/57wQou\u002BhGSGgKchbo4aNOg="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
17
start-server.bat
Normal file
17
start-server.bat
Normal 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
|
||||
Reference in New Issue
Block a user