Add device settings management and web panel sync API
Features:
- New DeviceSettings admin page at /devicesettings
- Manage device server configurations (URL, mode, deviceId)
- 3 new API endpoints for APK sync functionality
- UserSettings database model with SQLite storage
Implementation:
- ServerSettingsController.cs with getUserSettings, updateUserSettings, getAllUserSettings
- DeviceSettings.cshtml Razor page with add/edit/delete UI
- DeviceSettings.cshtml.cs page model with CRUD operations
- UserSettings model added to ApiModels.cs
- UserSettings DbSet added to RR3DbContext
- EF Core migration: 20260219180936_AddUserSettings
- Link added to Admin dashboard
API Endpoints:
- GET /api/settings/getUserSettings?deviceId={id} - APK sync endpoint
- POST /api/settings/updateUserSettings - Web panel update
- GET /api/settings/getAllUserSettings - Admin list view
Database Schema:
- UserSettings table (Id, DeviceId, ServerUrl, Mode, LastUpdated)
- SQLite storage with EF Core migrations
Integration:
- Works with APK SettingsActivity sync button
- Real-time configuration updates
- Emoji logging for all operations
- Device-specific server URL management
Usage:
1. Admin configures device settings at /devicesettings
2. User opens RR3 APK and taps Sync from Web Panel
3. APK downloads settings via API
4. Settings saved to SharedPreferences
5. Game restart applies configuration
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
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 Microsoft.EntityFrameworkCore;
|
||||||
|
using RR3CommunityServer.Models;
|
||||||
|
|
||||||
namespace RR3CommunityServer.Data;
|
namespace RR3CommunityServer.Data;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ public class RR3DbContext : DbContext
|
|||||||
public DbSet<CareerProgress> CareerProgress { get; set; }
|
public DbSet<CareerProgress> CareerProgress { get; set; }
|
||||||
public DbSet<GameAsset> GameAssets { get; set; }
|
public DbSet<GameAsset> GameAssets { get; set; }
|
||||||
public DbSet<ModPack> ModPacks { get; set; }
|
public DbSet<ModPack> ModPacks { get; set; }
|
||||||
|
public DbSet<UserSettings> UserSettings { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
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) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -704,10 +704,10 @@ namespace RR3CommunityServer.Migrations
|
|||||||
Active = true,
|
Active = true,
|
||||||
CarName = "Any Car",
|
CarName = "Any Car",
|
||||||
CashReward = 10000,
|
CashReward = 10000,
|
||||||
EndDate = new DateTime(2026, 2, 25, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5137),
|
EndDate = new DateTime(2026, 2, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3366),
|
||||||
GoldReward = 50,
|
GoldReward = 50,
|
||||||
Name = "Daily Sprint Challenge",
|
Name = "Daily Sprint Challenge",
|
||||||
StartDate = new DateTime(2026, 2, 18, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5134),
|
StartDate = new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3363),
|
||||||
TargetTime = 90.5,
|
TargetTime = 90.5,
|
||||||
TrackName = "Silverstone National"
|
TrackName = "Silverstone National"
|
||||||
},
|
},
|
||||||
@@ -717,10 +717,10 @@ namespace RR3CommunityServer.Migrations
|
|||||||
Active = true,
|
Active = true,
|
||||||
CarName = "Any Car",
|
CarName = "Any Car",
|
||||||
CashReward = 25000,
|
CashReward = 25000,
|
||||||
EndDate = new DateTime(2026, 2, 25, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5146),
|
EndDate = new DateTime(2026, 2, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375),
|
||||||
GoldReward = 100,
|
GoldReward = 100,
|
||||||
Name = "Speed Demon Trial",
|
Name = "Speed Demon Trial",
|
||||||
StartDate = new DateTime(2026, 2, 18, 9, 51, 0, 392, DateTimeKind.Utc).AddTicks(5146),
|
StartDate = new DateTime(2026, 2, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375),
|
||||||
TargetTime = 120.0,
|
TargetTime = 120.0,
|
||||||
TrackName = "Dubai Autodrome"
|
TrackName = "Dubai Autodrome"
|
||||||
});
|
});
|
||||||
@@ -797,6 +797,32 @@ namespace RR3CommunityServer.Migrations
|
|||||||
b.ToTable("Users");
|
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 =>
|
modelBuilder.Entity("RR3CommunityServer.Data.CareerProgress", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("RR3CommunityServer.Data.User", null)
|
b.HasOne("RR3CommunityServer.Data.User", null)
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
namespace RR3CommunityServer.Models;
|
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
|
// Progression request/response models
|
||||||
public class ProgressionUpdate
|
public class ProgressionUpdate
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,6 +109,9 @@
|
|||||||
<a href="/admin/purchases" class="btn btn-warning">
|
<a href="/admin/purchases" class="btn btn-warning">
|
||||||
<i class="bi bi-cart"></i> View Purchases
|
<i class="bi bi-cart"></i> View Purchases
|
||||||
</a>
|
</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">
|
<a href="/admin/settings" class="btn btn-secondary">
|
||||||
<i class="bi bi-gear"></i> Server Settings
|
<i class="bi bi-gear"></i> Server Settings
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
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>
|
||||||
|
}
|
||||||
108
RR3CommunityServer/Pages/DeviceSettings.cshtml.cs
Normal file
108
RR3CommunityServer/Pages/DeviceSettings.cshtml.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RR3CommunityServer.Data;
|
||||||
|
using RR3CommunityServer.Models;
|
||||||
|
|
||||||
|
namespace RR3CommunityServer.Pages;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user