diff --git a/RR3CommunityServer/Controllers/ServerSettingsController.cs b/RR3CommunityServer/Controllers/ServerSettingsController.cs new file mode 100644 index 0000000..3676b6f --- /dev/null +++ b/RR3CommunityServer/Controllers/ServerSettingsController.cs @@ -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 _logger; + + public ServerSettingsController(RR3DbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + /// + /// Get user settings (called by APK sync button) + /// GET /api/settings/getUserSettings?deviceId=xxx + /// + [HttpGet("getUserSettings")] + public async Task> 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" }); + } + } + + /// + /// Update user settings (called by web panel) + /// POST /api/settings/updateUserSettings + /// Body: { "deviceId": "xxx", "mode": "online", "serverUrl": "https://example.com:8443" } + /// + [HttpPost("updateUserSettings")] + public async Task> 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" }); + } + } + + /// + /// Get all user settings (for admin panel) + /// GET /api/settings/getAllUserSettings + /// + [HttpGet("getAllUserSettings")] + public async Task>> 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; } +} diff --git a/RR3CommunityServer/Data/RR3DbContext.cs b/RR3CommunityServer/Data/RR3DbContext.cs index a081da4..9f7fb17 100644 --- a/RR3CommunityServer/Data/RR3DbContext.cs +++ b/RR3CommunityServer/Data/RR3DbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using RR3CommunityServer.Models; namespace RR3CommunityServer.Data; @@ -20,6 +21,7 @@ public class RR3DbContext : DbContext public DbSet CareerProgress { get; set; } public DbSet GameAssets { get; set; } public DbSet ModPacks { get; set; } + public DbSet UserSettings { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/RR3CommunityServer/Migrations/20260219180936_AddUserSettings.Designer.cs b/RR3CommunityServer/Migrations/20260219180936_AddUserSettings.Designer.cs new file mode 100644 index 0000000..3d6a49d --- /dev/null +++ b/RR3CommunityServer/Migrations/20260219180936_AddUserSettings.Designer.cs @@ -0,0 +1,856 @@ +ο»Ώ// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Available") + .HasColumnType("INTEGER"); + + b.Property("BasePerformanceRating") + .HasColumnType("INTEGER"); + + b.Property("CarId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CashPrice") + .HasColumnType("INTEGER"); + + b.Property("ClassType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CustomAuthor") + .HasColumnType("TEXT"); + + b.Property("CustomVersion") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("GoldPrice") + .HasColumnType("INTEGER"); + + b.Property("IsCustom") + .HasColumnType("INTEGER"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CashCost") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("PerformanceIncrease") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BestTime") + .HasColumnType("REAL"); + + b.Property("Completed") + .HasColumnType("INTEGER"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StarsEarned") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("CareerProgress"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Available") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CashAmount") + .HasColumnType("INTEGER"); + + b.Property("Claimed") + .HasColumnType("INTEGER"); + + b.Property("ClaimedAt") + .HasColumnType("TEXT"); + + b.Property("GoldAmount") + .HasColumnType("INTEGER"); + + b.Property("RewardDate") + .HasColumnType("TEXT"); + + b.Property("Streak") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("DailyRewards"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("HardwareId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.GameAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AssetId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AssetType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CarId") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompressedSize") + .HasColumnType("INTEGER"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CustomAuthor") + .HasColumnType("TEXT"); + + b.Property("DownloadedAt") + .HasColumnType("TEXT"); + + b.Property("EaCdnPath") + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FileSha256") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsCustomContent") + .HasColumnType("INTEGER"); + + b.Property("LastAccessedAt") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Md5Hash") + .HasColumnType("TEXT"); + + b.Property("OriginalUrl") + .HasColumnType("TEXT"); + + b.Property("TrackId") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("GameAssets"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.ModPack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CarIds") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("DownloadCount") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PackId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("TrackIds") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ModPacks"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.OwnedCar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CarName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClassType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformanceRating") + .HasColumnType("INTEGER"); + + b.Property("PurchasedAt") + .HasColumnType("TEXT"); + + b.Property("PurchasedUpgrades") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpgradeLevel") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("OwnedCars"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.Purchase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("PurchaseTime") + .HasColumnType("TEXT"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SynergyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Purchases"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.Session", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SynergyId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.TimeTrial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CarName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CashReward") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("GoldReward") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("TargetTime") + .HasColumnType("REAL"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BeatTarget") + .HasColumnType("INTEGER"); + + b.Property("CashEarned") + .HasColumnType("INTEGER"); + + b.Property("GoldEarned") + .HasColumnType("INTEGER"); + + b.Property("SubmittedAt") + .HasColumnType("TEXT"); + + b.Property("TimeSeconds") + .HasColumnType("REAL"); + + b.Property("TimeTrialId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TimeTrialResults"); + }); + + modelBuilder.Entity("RR3CommunityServer.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Cash") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("Experience") + .HasColumnType("INTEGER"); + + b.Property("Gold") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("Reputation") + .HasColumnType("INTEGER"); + + b.Property("SynergyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RR3CommunityServer.Models.UserSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("Mode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/RR3CommunityServer/Migrations/20260219180936_AddUserSettings.cs b/RR3CommunityServer/Migrations/20260219180936_AddUserSettings.cs new file mode 100644 index 0000000..034df1d --- /dev/null +++ b/RR3CommunityServer/Migrations/20260219180936_AddUserSettings.cs @@ -0,0 +1,66 @@ +ο»Ώusing System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RR3CommunityServer.Migrations +{ + /// + public partial class AddUserSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserSettings", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DeviceId = table.Column(type: "TEXT", nullable: false), + ServerUrl = table.Column(type: "TEXT", nullable: false), + Mode = table.Column(type: "TEXT", nullable: false), + LastUpdated = table.Column(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) }); + } + + /// + 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) }); + } + } +} diff --git a/RR3CommunityServer/Migrations/RR3DbContextModelSnapshot.cs b/RR3CommunityServer/Migrations/RR3DbContextModelSnapshot.cs index dd644d2..b2449cf 100644 --- a/RR3CommunityServer/Migrations/RR3DbContextModelSnapshot.cs +++ b/RR3CommunityServer/Migrations/RR3DbContextModelSnapshot.cs @@ -704,10 +704,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, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3366), 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, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3363), TargetTime = 90.5, TrackName = "Silverstone National" }, @@ -717,10 +717,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, 26, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375), 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, 19, 18, 9, 35, 116, DateTimeKind.Utc).AddTicks(3375), TargetTime = 120.0, TrackName = "Dubai Autodrome" }); @@ -797,6 +797,32 @@ namespace RR3CommunityServer.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("RR3CommunityServer.Models.UserSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("Mode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserSettings"); + }); + modelBuilder.Entity("RR3CommunityServer.Data.CareerProgress", b => { b.HasOne("RR3CommunityServer.Data.User", null) diff --git a/RR3CommunityServer/Models/ApiModels.cs b/RR3CommunityServer/Models/ApiModels.cs index 36582be..948f821 100644 --- a/RR3CommunityServer/Models/ApiModels.cs +++ b/RR3CommunityServer/Models/ApiModels.cs @@ -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 { diff --git a/RR3CommunityServer/Pages/Admin.cshtml b/RR3CommunityServer/Pages/Admin.cshtml index 59a2be3..dc917c6 100644 --- a/RR3CommunityServer/Pages/Admin.cshtml +++ b/RR3CommunityServer/Pages/Admin.cshtml @@ -109,6 +109,9 @@ View Purchases + + Device Settings + Server Settings diff --git a/RR3CommunityServer/Pages/DeviceSettings.cshtml b/RR3CommunityServer/Pages/DeviceSettings.cshtml new file mode 100644 index 0000000..eb5d142 --- /dev/null +++ b/RR3CommunityServer/Pages/DeviceSettings.cshtml @@ -0,0 +1,201 @@ +@page +@model RR3CommunityServer.Pages.DeviceSettingsModel +@{ + ViewData["Title"] = "Device Server Settings"; +} + +
+
+
+
+
+

πŸ“± Device Server Settings

+

Configure server URLs for individual devices (syncs with APK)

+
+ ← Back to Dashboard +
+
+
+ + @if (TempData["Message"] != null) + { + + } + + +
+
+
+
+
βž• Add New Device Configuration
+
+
+
+
+
+ + + Enter the device ID from the APK +
+
+ + +
+
+ + + Include port if not 80/443 +
+
+ +
+
+
+
+ +
+
+
+
ℹ️ How It Works
+
    +
  1. Add device configuration here
  2. +
  3. User opens RR3 APK
  4. +
  5. User taps "πŸ”„ Sync from Web Panel"
  6. +
  7. APK fetches settings from this server
  8. +
  9. Game restarts with new settings
  10. +
+
+
+
+
+ + +
+
+
+
+
πŸ—‚οΈ Configured Devices (@Model.DeviceSettings.Count)
+
+
+ @if (Model.DeviceSettings.Count == 0) + { +
+ No device settings configured yet. Add one above to get started. +
+ } + else + { +
+ + + + + + + + + + + + @foreach (var setting in Model.DeviceSettings) + { + + + + + + + + } + +
Device IDModeServer URLLast UpdatedActions
@setting.DeviceId + @if (setting.Mode == "online") + { + 🌐 Online + } + else + { + πŸ“± Offline + } + + @if (!string.IsNullOrEmpty(setting.ServerUrl)) + { + @setting.ServerUrl + } + else + { + β€” + } + + + @setting.LastUpdated.ToLocalTime().ToString("MMM dd, yyyy HH:mm") + + +
+ + +
+
+
+ } +
+
+
+
+ + +
+
+
+
+
πŸ“š API Endpoints
+
+
+
GET /api/settings/getUserSettings?deviceId={deviceId}
+

Returns server configuration for a device (called by APK sync button)

+
{
+  "mode": "online",
+  "serverUrl": "https://rr3.example.com:8443",
+  "message": "Settings retrieved successfully"
+}
+ +
POST /api/settings/updateUserSettings
+

Update settings from web panel (this page uses it)

+
{
+  "deviceId": "device_abc123",
+  "mode": "online",
+  "serverUrl": "https://rr3.example.com:8443"
+}
+ +
GET /api/settings/getAllUserSettings
+

Get all device settings (admin only)

+
+
+
+
+
+ +@section Scripts { + +} diff --git a/RR3CommunityServer/Pages/DeviceSettings.cshtml.cs b/RR3CommunityServer/Pages/DeviceSettings.cshtml.cs new file mode 100644 index 0000000..d53d7a3 --- /dev/null +++ b/RR3CommunityServer/Pages/DeviceSettings.cshtml.cs @@ -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 _logger; + + public DeviceSettingsModel(RR3DbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public List 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 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 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(); + } +}