From 34a6ac92d3a3c4b614a7ccf9b85ccca6bcfc3e1a Mon Sep 17 00:00:00 2001 From: AlanMoonbase Date: Wed, 9 Jul 2025 14:51:18 -0700 Subject: [PATCH] Implement Store Backend MODEL CHANGE COMMANDS: `ALTER TABLE Users ADD ActiveProfileCosmetic int(11) NOT NULL;` `CREATE TABLE OwnedStoreItems (Id int(11) NOT NULL, UserId varchar(255) NOT NULL, StoreItemId int(11) NOT NULL, FOREIGN KEY (UserId) REFERENCES Users(Id));` --- qtc-net-server/Controllers/StoreController.cs | 54 ++++++++++++ qtc-net-server/Data/DataContext.cs | 9 ++ .../Dtos/User/UserInformationDto.cs | 1 + .../Dtos/User/UserUpdateInformationDto.cs | 1 + qtc-net-server/Enums/StoreItemType.cs | 8 ++ qtc-net-server/Models/OwnedStoreItem.cs | 14 ++++ qtc-net-server/Models/User.cs | 2 + qtc-net-server/Program.cs | 3 + qtc-net-server/Resources/store.json | 11 +++ qtc-net-server/Schema/StoreItem.cs | 23 ++++++ .../Services/StoreService/StoreService.cs | 82 +++++++++++++++++++ .../Services/UserService/UserService.cs | 2 + qtc-net-server/qtc-net-server.csproj | 3 + 13 files changed, 213 insertions(+) create mode 100644 qtc-net-server/Controllers/StoreController.cs create mode 100644 qtc-net-server/Enums/StoreItemType.cs create mode 100644 qtc-net-server/Models/OwnedStoreItem.cs create mode 100644 qtc-net-server/Resources/store.json create mode 100644 qtc-net-server/Schema/StoreItem.cs create mode 100644 qtc-net-server/Services/StoreService/StoreService.cs diff --git a/qtc-net-server/Controllers/StoreController.cs b/qtc-net-server/Controllers/StoreController.cs new file mode 100644 index 0000000..da957b3 --- /dev/null +++ b/qtc-net-server/Controllers/StoreController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using qtc_api.Services.StoreService; +using System.Security.Claims; + +namespace qtc_api.Controllers +{ + [Route("api/store")] + [ApiController] + public class StoreController : ControllerBase + { + private readonly StoreService _storeService; + private readonly IUserService _userService; + public StoreController(StoreService storeService, IUserService userService) + { + _storeService = storeService; + _userService = userService; + } + + [HttpGet] + [Route("all-items")] + public ActionResult>> GetAllItems() + { + return Ok(_storeService.GetStoreItems()); + } + + [HttpPost] + [Route("buy-item")] + [Authorize] + public async Task>> BuyStoreItem(int id) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if (identity != null) + { + IEnumerable claims = identity.Claims; + var userId = claims.First().Value; + + if (userId != null) + { + var user = await _userService.GetUserById(userId); + if(user != null && user.Success && user.Data != null) + { + var result = await _storeService.BuyStoreItem(user.Data.Id, id); + return Ok(result); + } + else return Ok(new ServiceResponse { Success = false, Message = "User Not Found In Auth Header" }); + } + else return Ok(new ServiceResponse { Success = false, Message = "No UserId In Auth Header" }); + } + else return Ok(new ServiceResponse { Success = false, Message = "No Auth Header" }); + } + } +} diff --git a/qtc-net-server/Data/DataContext.cs b/qtc-net-server/Data/DataContext.cs index 7780ea0..3430ee3 100644 --- a/qtc-net-server/Data/DataContext.cs +++ b/qtc-net-server/Data/DataContext.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Options; +using Org.BouncyCastle.Asn1.Nist; namespace qtc_api.Data { @@ -13,6 +14,7 @@ namespace qtc_api.Data public DbSet Rooms { get; set; } public DbSet ValidRefreshTokens { get; set; } public DbSet Contacts { get; set; } + public DbSet OwnedStoreItems { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) { @@ -40,6 +42,7 @@ namespace qtc_api.Data builder.Entity().HasMany(e => e.ContactsList); builder.Entity().HasMany(e => e.ContactsMade); + builder.Entity().HasMany(e => e.OwnedStoreItems); // Rooms (no relations) @@ -60,6 +63,12 @@ namespace qtc_api.Data builder.Entity().HasOne(e => e.User) .WithMany(e => e.ContactsList) .HasForeignKey(e => e.UserId); + + // Purchased Store Items + + builder.Entity().HasOne(e => e.User) + .WithMany(e => e.OwnedStoreItems) + .HasForeignKey(e => e.UserId); } } } diff --git a/qtc-net-server/Dtos/User/UserInformationDto.cs b/qtc-net-server/Dtos/User/UserInformationDto.cs index eafac9a..89350df 100644 --- a/qtc-net-server/Dtos/User/UserInformationDto.cs +++ b/qtc-net-server/Dtos/User/UserInformationDto.cs @@ -11,5 +11,6 @@ public DateTime CreatedAt { get; set; } = new DateTime(); public int Status { get; set; } = 0; public int CurrencyAmount { get; set; } = 0; + public int ProfileCosmeticId { get; set; } = 0; } } diff --git a/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs b/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs index a97cc3d..461addd 100644 --- a/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs +++ b/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs @@ -6,5 +6,6 @@ public string Username { get; set; } = string.Empty; public string Bio { get; set; } = string.Empty; public DateTime DateOfBirth { get; set; } = new DateTime(); + public int ProfileCosmeticId { get; set; } = 0; } } diff --git a/qtc-net-server/Enums/StoreItemType.cs b/qtc-net-server/Enums/StoreItemType.cs new file mode 100644 index 0000000..26359c2 --- /dev/null +++ b/qtc-net-server/Enums/StoreItemType.cs @@ -0,0 +1,8 @@ +namespace qtc_api.Enums +{ + public enum StoreItemType + { + ProfileCosmetic = 1, + ClientCosmetic = 2 + } +} diff --git a/qtc-net-server/Models/OwnedStoreItem.cs b/qtc-net-server/Models/OwnedStoreItem.cs new file mode 100644 index 0000000..31c973d --- /dev/null +++ b/qtc-net-server/Models/OwnedStoreItem.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace qtc_api.Models +{ + public class OwnedStoreItem + { + [Key] + public int Id { get; set; } + public string UserId { get; set; } = string.Empty; + public int StoreItemId { get; set; } + + public virtual User? User { get; } + } +} diff --git a/qtc-net-server/Models/User.cs b/qtc-net-server/Models/User.cs index 594c039..04f39e0 100644 --- a/qtc-net-server/Models/User.cs +++ b/qtc-net-server/Models/User.cs @@ -15,9 +15,11 @@ public int CurrencyAmount { get; set; } = 0; public int StockAmount { get; set; } = 0; public DateTime LastCurrencySpin { get; set; } + public int ActiveProfileCosmetic { get; set; } = 0; public virtual IEnumerable? RefreshTokens { get; } public virtual IEnumerable? ContactsMade { get; } public virtual IEnumerable? ContactsList { get; } + public virtual IEnumerable? OwnedStoreItems { get; } } } diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs index 9aa0175..3490c2b 100644 --- a/qtc-net-server/Program.cs +++ b/qtc-net-server/Program.cs @@ -3,6 +3,7 @@ global using Microsoft.EntityFrameworkCore; global using Microsoft.AspNetCore.SignalR; global using Microsoft.AspNetCore.Authorization; global using qtc_api.Models; +global using qtc_api.Schema; global using qtc_api.Data; global using qtc_api.Dtos.User; global using qtc_api.Dtos.Room; @@ -15,6 +16,7 @@ using qtc_api.Services.RoomService; using qtc_api.Services.ContactService; using qtc_api.Services.CurrencyGamesService; using qtc_api.Services.GameRoomService; +using qtc_api.Services.StoreService; var builder = WebApplication.CreateBuilder(args); @@ -57,6 +59,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/qtc-net-server/Resources/store.json b/qtc-net-server/Resources/store.json new file mode 100644 index 0000000..3d0b7e7 --- /dev/null +++ b/qtc-net-server/Resources/store.json @@ -0,0 +1,11 @@ +[ + { + "Id": 1, + "Type": 1, + "Price": 100, + "Name": "Exmaple", + "Description": "Change Me!", + "AssetUrl": "https://cdn.alanmoon.net/qtc/cosmetics/test/test.gif", + "ThumbnailUrl": "https://cdn.alanmoon.net/qtc/cosmetics/test/thumbnail.jpg" + } +] diff --git a/qtc-net-server/Schema/StoreItem.cs b/qtc-net-server/Schema/StoreItem.cs new file mode 100644 index 0000000..b7a5dc5 --- /dev/null +++ b/qtc-net-server/Schema/StoreItem.cs @@ -0,0 +1,23 @@ +using qtc_api.Enums; +using System.Text.Json.Serialization; + +namespace qtc_api.Schema +{ + public class StoreItem + { + [JsonPropertyName("Id")] + public int Id { get; set; } + [JsonPropertyName("Type")] + public StoreItemType Type { get; set; } + [JsonPropertyName("Price")] + public int Price { get; set; } + [JsonPropertyName("Name")] + public string Name { get; set; } = string.Empty; + [JsonPropertyName("Description")] + public string Description { get; set; } = string.Empty; + [JsonPropertyName("AssetUrl")] + public string AssetUrl { get; set; } = string.Empty; + [JsonPropertyName("ThumbnailUrl")] + public string ThumbnailUrl { get; set; } = string.Empty; + } +} diff --git a/qtc-net-server/Services/StoreService/StoreService.cs b/qtc-net-server/Services/StoreService/StoreService.cs new file mode 100644 index 0000000..2a29a22 --- /dev/null +++ b/qtc-net-server/Services/StoreService/StoreService.cs @@ -0,0 +1,82 @@ +using System.Text.Json; + +namespace qtc_api.Services.StoreService +{ + public class StoreService + { + public static List StoreItems { get; set; } = []; + + private readonly DataContext _ctx; + private readonly IUserService _userService; + public StoreService(DataContext ctx, IUserService userService) + { + _ctx = ctx; + _userService = userService; + + StoreItem[]? storeItems = JsonSerializer.Deserialize(File.ReadAllText("./Resources/store.json")); + if (storeItems != null && storeItems.Length > 0) + { + StoreItems = storeItems.ToList(); + } + } + + public ServiceResponse> GetStoreItems() + { + return new ServiceResponse> { Success = true, Data = StoreItems }; + } + + public ServiceResponse GetBoughtStoreItemFromUser(string userId, int itemId) + { + // find item owned by user + var item = _ctx.OwnedStoreItems.FirstOrDefault(e => e.UserId == userId && e.StoreItemId == itemId); + if (item != null) + return new ServiceResponse { Success = true, Data = item }; + else return new ServiceResponse { Success = false, Message = "Item Not Yet Purchased" }; + } + + public ServiceResponse> GetBoughtStoreItemsFromUser(string userId) + { + // find items owned by user + var items = _ctx.OwnedStoreItems.Where(e => e.UserId == userId).ToList(); + if (items != null && items.Count > 0) + return new ServiceResponse> { Success = true, Data = items }; + else return new ServiceResponse> { Success = false, Message = "User Owns No Items" }; + } + + public async Task> BuyStoreItem(string userId, int id) + { + // find item in store + var item = StoreItems.FirstOrDefault(e => e.Id == id); + if (item != null) + { + // deduct currency from user + var user = await _userService.GetUserById(userId); + if (user != null && user.Success && user.Data != null) + { + if (user.Data.CurrencyAmount >= item.Price) + { + // remove currency from user + await _userService.RemoveCurrencyFromUser(userId, item.Price); + + // create owned item + OwnedStoreItem ownedStoreItem = new OwnedStoreItem + { + StoreItemId = item.Id, + UserId = userId + }; + + // add to table + _ctx.OwnedStoreItems.Add(ownedStoreItem); + await _ctx.SaveChangesAsync(); + + // return successful service response + return new ServiceResponse { Success = true, Data = ownedStoreItem }; + } + else return new ServiceResponse { Success = false, Message = "Insufficient Currency" }; + } + else return new ServiceResponse { Success = false, Message = "User Not Found" }; + } + else return new ServiceResponse { Success = false, Message = "Item Not Found" }; + } + } +} diff --git a/qtc-net-server/Services/UserService/UserService.cs b/qtc-net-server/Services/UserService/UserService.cs index fdcf64e..ac648a8 100644 --- a/qtc-net-server/Services/UserService/UserService.cs +++ b/qtc-net-server/Services/UserService/UserService.cs @@ -174,6 +174,7 @@ dto.CreatedAt = user.CreatedAt; dto.Status = user.Status; dto.CurrencyAmount = user.CurrencyAmount; + dto.ProfileCosmeticId = user.ActiveProfileCosmetic; serviceResponse.Success = true; serviceResponse.Data = dto; @@ -219,6 +220,7 @@ infoDto.CreatedAt = dbUser.CreatedAt; infoDto.Status = dbUser.Status; infoDto.CurrencyAmount = dbUser.CurrencyAmount; + infoDto.ProfileCosmeticId = request.ProfileCosmeticId; serviceResponse.Success = true; serviceResponse.Data = infoDto; diff --git a/qtc-net-server/qtc-net-server.csproj b/qtc-net-server/qtc-net-server.csproj index eae8b2e..e297e34 100644 --- a/qtc-net-server/qtc-net-server.csproj +++ b/qtc-net-server/qtc-net-server.csproj @@ -31,6 +31,9 @@ + + Always + Always