From c0a94f26d632d662b30dc58cbf89695e7cde4c9a Mon Sep 17 00:00:00 2001 From: hz Date: Thu, 8 May 2025 09:29:20 -0500 Subject: [PATCH] Commit 0 --- Controllers/AuthController.cs | 139 ++++++++++++ Controllers/ItemController.cs | 39 ++++ Controllers/LocationController.cs | 78 +++++++ Controllers/PlayerController.cs | 282 +++++++++++++++++++++++++ Controllers/SpawnableController.cs | 41 ++++ GameAuthApi.csproj | 11 + GameAuthApi.sln | 24 +++ Middleware/TokenBlacklistMiddleware.cs | 33 +++ Models/BlacklistedToken.cs | 14 ++ Models/DTOs/ChangeRoleRequest.cs | 7 + Models/DTOs/CreateItemRequest.cs | 8 + Models/DTOs/CreateLocationRequest.cs | 17 ++ Models/DTOs/CreatePlayerRequest.cs | 8 + Models/DTOs/CreateSpawnableRequest.cs | 7 + Models/DTOs/LoginRequest.cs | 7 + Models/DTOs/MoveRequest.cs | 6 + Models/DTOs/RegisterRequest.cs | 8 + Models/DTOs/TeleportRequest.cs | 7 + Models/Item.cs | 16 ++ Models/Location.cs | 40 ++++ Models/Player.cs | 18 ++ Models/Spawnable.cs | 14 ++ Models/User.cs | 19 ++ Program.cs | 83 ++++++++ Services/BlacklistService.cs | 31 +++ Services/ItemService.cs | 39 ++++ Services/LocationService.cs | 69 ++++++ Services/PlayerService.cs | 44 ++++ Services/SpawnableService.cs | 29 +++ Services/UserService.cs | 27 +++ appsettings.json | 16 ++ 31 files changed, 1181 insertions(+) create mode 100644 Controllers/AuthController.cs create mode 100644 Controllers/ItemController.cs create mode 100644 Controllers/LocationController.cs create mode 100644 Controllers/PlayerController.cs create mode 100644 Controllers/SpawnableController.cs create mode 100644 GameAuthApi.csproj create mode 100644 GameAuthApi.sln create mode 100644 Middleware/TokenBlacklistMiddleware.cs create mode 100644 Models/BlacklistedToken.cs create mode 100644 Models/DTOs/ChangeRoleRequest.cs create mode 100644 Models/DTOs/CreateItemRequest.cs create mode 100644 Models/DTOs/CreateLocationRequest.cs create mode 100644 Models/DTOs/CreatePlayerRequest.cs create mode 100644 Models/DTOs/CreateSpawnableRequest.cs create mode 100644 Models/DTOs/LoginRequest.cs create mode 100644 Models/DTOs/MoveRequest.cs create mode 100644 Models/DTOs/RegisterRequest.cs create mode 100644 Models/DTOs/TeleportRequest.cs create mode 100644 Models/Item.cs create mode 100644 Models/Location.cs create mode 100644 Models/Player.cs create mode 100644 Models/Spawnable.cs create mode 100644 Models/User.cs create mode 100644 Program.cs create mode 100644 Services/BlacklistService.cs create mode 100644 Services/ItemService.cs create mode 100644 Services/LocationService.cs create mode 100644 Services/PlayerService.cs create mode 100644 Services/SpawnableService.cs create mode 100644 Services/UserService.cs create mode 100644 appsettings.json diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs new file mode 100644 index 0000000..0fb5967 --- /dev/null +++ b/Controllers/AuthController.cs @@ -0,0 +1,139 @@ +using GameAuthApi.Models; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace GameAuthApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly UserService _userService; + private readonly IConfiguration _config; + private readonly BlacklistService _blacklistService; + + public AuthController(UserService userService, IConfiguration config, BlacklistService blacklistService) + { + _userService = userService; + _config = config; + _blacklistService = blacklistService; + } + + [HttpPost("register")] + public async Task Register([FromBody] RegisterRequest credentials) + { + if (await _userService.GetByUsernameAsync(credentials.Username) != null) + return BadRequest("User already exists"); + + var hash = BCrypt.Net.BCrypt.HashPassword(credentials.Password); + + var user = new User + { + Username = credentials.Username, + PasswordHash = hash, + Role = "USER", + Email = credentials.Email + }; + + await _userService.CreateAsync(user); + return Ok("User created"); + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginRequest credentials) + { + var user = await _userService.GetByUsernameAsync(credentials.Username); + if (user == null || !BCrypt.Net.BCrypt.Verify(credentials.Password, user.PasswordHash)) + return Unauthorized(); + + var accessToken = GenerateJwtToken(user); + var refreshToken = Guid.NewGuid().ToString(); + var refreshExpiry = DateTime.UtcNow.AddDays(7); + + user.RefreshToken = refreshToken; + user.RefreshTokenExpiry = refreshExpiry; + await _userService.UpdateAsync(user); + + return Ok(new { accessToken, refreshToken }); + } + + [HttpPost("refresh")] + public async Task Refresh([FromBody] RefreshRequest req) + { + var user = await _userService.GetByUsernameAsync(req.Username); + if (user == null || user.RefreshToken != req.RefreshToken || user.RefreshTokenExpiry < DateTime.UtcNow) + return Unauthorized("Invalid or expired refresh token"); + + var newAccessToken = GenerateJwtToken(user); + return Ok(new { accessToken = newAccessToken }); + } + + [HttpPost("logout")] + [Authorize(Roles = "USER,SUPER")] + public async Task Logout() + { + var token = HttpContext.Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", ""); + if (string.IsNullOrWhiteSpace(token)) + return BadRequest("Token missing"); + + var jwtToken = new JwtSecurityTokenHandler().ReadToken(token) as JwtSecurityToken; + var expiry = jwtToken.ValidTo; + + await _blacklistService.AddToBlacklistAsync(token, expiry); + return Ok("Token blacklisted, user logged out."); + } + + [HttpPost("role")] + [Authorize(Roles = "SUPER")] + public async Task ChangeUserRole([FromBody] ChangeRoleRequest request) + { + if (request.NewRole != "USER" && request.NewRole != "SUPER") + return BadRequest("Role must be 'USER' or 'SUPER'"); + + var user = await _userService.GetByUsernameAsync(request.Username); + if (user == null) + return NotFound("User not found"); + + user.Role = request.NewRole; + await _userService.UpdateAsync(user); + + return Ok($"{request.Username}'s role updated to {request.NewRole}"); + } + + private string GenerateJwtToken(User user) + { + var key = Encoding.UTF8.GetBytes(_config["Jwt:Key"]); + var creds = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.Name, user.Username), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Role, user.Role) + }; + + var token = new JwtSecurityToken( + _config["Jwt:Issuer"], + _config["Jwt:Issuer"], + claims, + expires: DateTime.UtcNow.AddMinutes(15), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public class RefreshRequest + { + public string Username { get; set; } + public string RefreshToken { get; set; } + } +} \ No newline at end of file diff --git a/Controllers/ItemController.cs b/Controllers/ItemController.cs new file mode 100644 index 0000000..1740fa2 --- /dev/null +++ b/Controllers/ItemController.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using GameAuthApi.Models; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace GameAuthApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ItemController : ControllerBase +{ + private readonly ItemService _itemService; + private readonly LocationService _locationService; + + public ItemController(ItemService itemService, LocationService locationService) + { + _itemService = itemService; + _locationService = locationService; + } + + [HttpPost] + [Authorize(Roles = "SUPER")] + public async Task Create([FromBody] CreateItemRequest req) + { + var location = await _locationService.GetByIdAsync(req.LocationId); + if (location == null) + return BadRequest("Invalid location ID."); + + var item = new Item + { + Name = req.Name, + LocationId = req.LocationId + }; + + await _itemService.CreateAsync(item); + return Ok(item); + } +} diff --git a/Controllers/LocationController.cs b/Controllers/LocationController.cs new file mode 100644 index 0000000..7d7274f --- /dev/null +++ b/Controllers/LocationController.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GameAuthApi.Models; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace GameAuthApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class LocationController : ControllerBase +{ + private readonly LocationService _locationService; + private readonly PlayerService _playerService; + + public LocationController(LocationService locationService, PlayerService playerService) + { + _locationService = locationService; + _playerService = playerService; + } + + [HttpPost] + [Authorize(Roles = "SUPER")] + public async Task Create([FromBody] CreateLocationRequest req) + { + var newLoc = new Location + { + X = req.X, + Y = req.Y, + Z = req.Z, + N = req.N, + NE = req.NE, + E = req.E, + SE = req.SE, + S = req.S, + SW = req.SW, + W = req.W, + NW = req.NW + }; + + await _locationService.CreateAsync(newLoc); + + return Ok(newLoc); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "SUPER")] + public async Task Delete(string id) + { + var location = await _locationService.GetByIdAsync(id); + if (location == null) + return NotFound("Location not found."); + + // Null out all players at this location + var affectedPlayers = await _playerService.GetByLocationIdAsync(id); + foreach (var player in affectedPlayers) + { + player.LocationId = null; + await _playerService.UpdateAsync(player); + } + + await _locationService.DeleteAsync(id); + return Ok($"Location deleted and {affectedPlayers.Count} player(s) unlinked."); + } + + [HttpGet("{id}")] + [Authorize(Roles = "USER")] + public async Task GetById(string id) + { + var location = await _locationService.GetByIdAsync(id); + if (location == null) + return NotFound("Location not found"); + + return Ok(location); + } +} diff --git a/Controllers/PlayerController.cs b/Controllers/PlayerController.cs new file mode 100644 index 0000000..1892a89 --- /dev/null +++ b/Controllers/PlayerController.cs @@ -0,0 +1,282 @@ +// Controllers/PlayerController.cs +using GameAuthApi.Models; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GameAuthApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class PlayerController : ControllerBase +{ + private readonly PlayerService _playerService; + private readonly LocationService _locationService; + private readonly SpawnableService _spawnableService; + private readonly ItemService _itemService; + + public PlayerController(PlayerService playerService, LocationService locationService, SpawnableService spawnableService, ItemService itemService) + { + _playerService = playerService; + _locationService = locationService; + _spawnableService = spawnableService; + _itemService = itemService; + } + + [HttpPost] + [Authorize(Roles = "USER,SUPER")] + public async Task Create([FromBody] CreatePlayerRequest req) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(userId)) + return Unauthorized("User ID not found in token."); + + var existing = await _playerService.GetByUserIdAsync(userId); + if (existing != null) + return BadRequest("Player already exists for this user."); + + Location spawnLocation = null; + + if (!string.IsNullOrWhiteSpace(req.LocationId)) + { + spawnLocation = await _locationService.GetByIdAsync(req.LocationId); + if (spawnLocation == null) + return BadRequest("Specified location does not exist."); + } + else + { + spawnLocation = await _locationService.GetSpawnLocationAsync(); + // If not found, spawnLocation remains null + } + + // create the inventory location + var inventoryLocation = new Location + { + LocationType = "Inventory", + X = null, + Y = null, + Z = null, + N = null, + NE = null, + E = null, + SE = null, + S = null, + SW = null, + W = null, + NW = null, + Players = new List() // optional + }; + + await _locationService.CreateAsync(inventoryLocation); + + var player = new Player + { + UserId = userId, + Name = req.Name, + LocationId = spawnLocation?.Id, + InventoryLocationId = inventoryLocation.Id, + Climbing = 3 + }; + + await _playerService.CreateAsync(player); + + if (spawnLocation != null) + { + await _locationService.AddPlayerToLocationAsync(spawnLocation.Id, player.Id); + } + + return Ok(player); + } + + [HttpDelete] + [Authorize(Roles = "USER,SUPER")] + public async Task Delete() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var role = User.FindFirstValue(ClaimTypes.Role); + + var player = await _playerService.GetByUserIdAsync(userId); + if (player == null) + return NotFound("No player found for this user."); + + // Remove from current location if set + if (!string.IsNullOrEmpty(player.LocationId)) + { + await _locationService.RemovePlayerFromLocationAsync(player.LocationId, player.Id); + } + + await _playerService.DeleteAsync(player.Id); + return Ok($"Player '{player.Name}' deleted."); + } + + [HttpPost("teleport")] + [Authorize(Roles = "SUPER")] + public async Task AdminTeleport([FromBody] TeleportRequest req) + { + var player = await _playerService.GetByIdAsync(req.PlayerId); + if (player == null) + return NotFound("Player not found."); + + var newLocation = await _locationService.GetByIdAsync(req.LocationId); + if (newLocation == null) + return BadRequest("Target location does not exist."); + + if (!string.IsNullOrEmpty(player.LocationId)) + { + await _locationService.RemovePlayerFromLocationAsync(player.LocationId, player.Id); + } + + player.LocationId = newLocation.Id; + await _playerService.UpdateAsync(player); + await _locationService.AddPlayerToLocationAsync(newLocation.Id, player.Id); + + return Ok($"Player '{player.Name}' teleported to '{newLocation.X}, {newLocation.Y}'."); + } + + [HttpPost("move")] + [Authorize(Roles = "USER,SUPER")] + public async Task Move([FromBody] MoveRequest req) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var player = await _playerService.GetByUserIdAsync(userId); + if (player == null) + return NotFound("Player not found."); + + if (string.IsNullOrWhiteSpace(player.LocationId)) + return BadRequest("Player is not currently in a location."); + + var currentLocation = await _locationService.GetByIdAsync(player.LocationId); + if (currentLocation == null) + return BadRequest("Current location not found."); + + var direction = req.Direction.ToUpper(); + var directionMap = new Dictionary + { + { "N", (0, 1, "S") }, { "NE", (1, 1, "SW") }, { "E", (1, 0, "W") }, { "SE", (1, -1, "NW") }, + { "S", (0, -1, "N") }, { "SW", (-1, -1, "NE") }, { "W", (-1, 0, "E") }, { "NW", (-1, 1, "SE") } + }; + + if (!directionMap.ContainsKey(direction)) + return BadRequest("Invalid direction. Use N, NE, E, SE, S, SW, W, or NW."); + + var (dx, dy, reverseDir) = directionMap[direction]; + + var targetId = typeof(Location).GetProperty(direction)?.GetValue(currentLocation)?.ToString(); + Location targetLocation; + + if (!string.IsNullOrWhiteSpace(targetId)) + { + targetLocation = await _locationService.GetByIdAsync(targetId); + if (targetLocation == null) + return BadRequest("Linked location ID is invalid."); + + var z1 = currentLocation.Z ?? 0; + var z2 = targetLocation.Z ?? 0; + var zDiff = Math.Abs(z1 - z2); + + if (zDiff > player.Climbing) + { + return BadRequest($"Cannot move {req.Direction.ToUpper()}: elevation difference ({zDiff}) exceeds your climbing ability ({player.Climbing})."); + } + } + else + { + // Generate new location + var newX = currentLocation.X + dx; + var newY = currentLocation.Y + dy; + var random = new Random(); + var newZ = currentLocation.Z + random.Next(-1, 2); // -1, 0, or +1 + + string itemId = null; + var spawnables = await _spawnableService.GetAllAsync(); + + if (spawnables.Any()) + { + var rng = new Random(); + var roll = rng.Next(0, 100); // 0–99 + + foreach (var s in spawnables.OrderBy(s => s.SpawnRate)) + { + if (roll < s.SpawnRate) + { + var tempItem = new Item + { + Name = s.Name, + LocationId = null + }; + + await _itemService.CreateAsync(tempItem); + itemId = tempItem.Id; + break; + } + } + } + + targetLocation = new Location + { + LocationType = "World", + X = newX, + Y = newY, + Z = newZ, + Content = itemId != null ? [itemId] : [] + }; + + await _locationService.CreateAsync(targetLocation); + + if (itemId != null) + { + var createdItem = await _itemService.GetByIdAsync(itemId); + createdItem.LocationId = targetLocation.Id; + await _itemService.UpdateAsync(createdItem); + } + + // Link current → new + typeof(Location).GetProperty(direction)?.SetValue(currentLocation, targetLocation.Id); + // Link new → current + typeof(Location).GetProperty(reverseDir)?.SetValue(targetLocation, currentLocation.Id); + + await _locationService.UpdateAsync(currentLocation); + await _locationService.UpdateAsync(targetLocation); + + // Link additional neighbors + foreach (var entry in directionMap) + { + var dir = entry.Key; + var (nx, ny, rev) = entry.Value; + + var neighbor = await _locationService.GetByCoordinatesAsync( + (targetLocation.X ?? 0) + nx, + (targetLocation.Y ?? 0) + ny, + targetLocation.Z ?? 0); + if (neighbor != null) + { + typeof(Location).GetProperty(dir)?.SetValue(targetLocation, neighbor.Id); + typeof(Location).GetProperty(rev)?.SetValue(neighbor, targetLocation.Id); + await _locationService.UpdateAsync(neighbor); + } + } + + await _locationService.UpdateAsync(targetLocation); + } + + // Move the player + await _locationService.RemovePlayerFromLocationAsync(currentLocation.Id, player.Id); + await _locationService.AddPlayerToLocationAsync(targetLocation.Id, player.Id); + + player.LocationId = targetLocation.Id; + await _playerService.UpdateAsync(player); + + return Ok($"Player '{player.Name}' moved {direction} to '{targetLocation.X}, {targetLocation.Y}'."); + } + + private static readonly Dictionary DirectionData = new() + { + { "N", (0, 1, "S") }, { "NE", (1, 1, "SW") }, { "E", (1, 0, "W") }, { "SE", (1, -1, "NW") }, + { "S", (0, -1, "N") }, { "SW", (-1, -1, "NE") }, { "W", (-1, 0, "E") }, { "NW", (-1, 1, "SE") } + }; +} diff --git a/Controllers/SpawnableController.cs b/Controllers/SpawnableController.cs new file mode 100644 index 0000000..ae3a7c9 --- /dev/null +++ b/Controllers/SpawnableController.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using GameAuthApi.Models; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace GameAuthApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class SpawnableController : ControllerBase +{ + private readonly SpawnableService _spawnableService; + + public SpawnableController(SpawnableService spawnableService) + { + _spawnableService = spawnableService; + } + + [HttpPost] + [Authorize(Roles = "SUPER")] + public async Task Create([FromBody] CreateSpawnableRequest req) + { + var spawnable = new Spawnable + { + Name = req.Name.ToLower(), + SpawnRate = req.SpawnRate + }; + + await _spawnableService.CreateAsync(spawnable); + return Ok(spawnable); + } + + [HttpGet] + [Authorize(Roles = "SUPER")] + public async Task GetAll() + { + var all = await _spawnableService.GetAllAsync(); + return Ok(all); + } +} diff --git a/GameAuthApi.csproj b/GameAuthApi.csproj new file mode 100644 index 0000000..e0fd56d --- /dev/null +++ b/GameAuthApi.csproj @@ -0,0 +1,11 @@ + + + net8.0 + + + + + + + + \ No newline at end of file diff --git a/GameAuthApi.sln b/GameAuthApi.sln new file mode 100644 index 0000000..5cefa76 --- /dev/null +++ b/GameAuthApi.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameAuthApi", "GameAuthApi.csproj", "{349B73A5-7632-5ADF-4645-BB2EF73FA948}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {349B73A5-7632-5ADF-4645-BB2EF73FA948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {349B73A5-7632-5ADF-4645-BB2EF73FA948}.Debug|Any CPU.Build.0 = Debug|Any CPU + {349B73A5-7632-5ADF-4645-BB2EF73FA948}.Release|Any CPU.ActiveCfg = Release|Any CPU + {349B73A5-7632-5ADF-4645-BB2EF73FA948}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7EFA1A51-A917-4BB3-9C99-E90800825E9F} + EndGlobalSection +EndGlobal diff --git a/Middleware/TokenBlacklistMiddleware.cs b/Middleware/TokenBlacklistMiddleware.cs new file mode 100644 index 0000000..c5a3049 --- /dev/null +++ b/Middleware/TokenBlacklistMiddleware.cs @@ -0,0 +1,33 @@ +using System.Linq; +using System.Threading.Tasks; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Http; + +namespace GameAuthApi.Middleware; + +public class TokenBlacklistMiddleware +{ + private readonly RequestDelegate _next; + + public TokenBlacklistMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context, BlacklistService blacklistService) + { + var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", ""); + + if (!string.IsNullOrWhiteSpace(token)) + { + if (await blacklistService.IsTokenBlacklistedAsync(token)) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Token has been revoked"); + return; + } + } + + await _next(context); + } +} \ No newline at end of file diff --git a/Models/BlacklistedToken.cs b/Models/BlacklistedToken.cs new file mode 100644 index 0000000..e5d2d83 --- /dev/null +++ b/Models/BlacklistedToken.cs @@ -0,0 +1,14 @@ +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace GameAuthApi.Models; + +public class BlacklistedToken +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string Token { get; set; } + public DateTime Expiry { get; set; } +} \ No newline at end of file diff --git a/Models/DTOs/ChangeRoleRequest.cs b/Models/DTOs/ChangeRoleRequest.cs new file mode 100644 index 0000000..2cc9009 --- /dev/null +++ b/Models/DTOs/ChangeRoleRequest.cs @@ -0,0 +1,7 @@ +namespace GameAuthApi.Models; + +public class ChangeRoleRequest +{ + public string Username { get; set; } + public string NewRole { get; set; } +} diff --git a/Models/DTOs/CreateItemRequest.cs b/Models/DTOs/CreateItemRequest.cs new file mode 100644 index 0000000..1c658f6 --- /dev/null +++ b/Models/DTOs/CreateItemRequest.cs @@ -0,0 +1,8 @@ +namespace GameAuthApi.Models; + +public class CreateItemRequest +{ + public string LocationId { get; set; } + + public string Name { get; set; } +} diff --git a/Models/DTOs/CreateLocationRequest.cs b/Models/DTOs/CreateLocationRequest.cs new file mode 100644 index 0000000..bc5398a --- /dev/null +++ b/Models/DTOs/CreateLocationRequest.cs @@ -0,0 +1,17 @@ +namespace GameAuthApi.Models; + +public class CreateLocationRequest +{ + public int X { get; set; } + public int Y { get; set; } + public int Z { get; set; } + + public string N { get; set; } + public string NE { get; set; } + public string E { get; set; } + public string SE { get; set; } + public string S { get; set; } + public string SW { get; set; } + public string W { get; set; } + public string NW { get; set; } +} \ No newline at end of file diff --git a/Models/DTOs/CreatePlayerRequest.cs b/Models/DTOs/CreatePlayerRequest.cs new file mode 100644 index 0000000..e486cbe --- /dev/null +++ b/Models/DTOs/CreatePlayerRequest.cs @@ -0,0 +1,8 @@ +// Models/CreatePlayerRequest.cs +namespace GameAuthApi.Models; + +public class CreatePlayerRequest +{ + public string Name { get; set; } + public string LocationId { get; set; } +} diff --git a/Models/DTOs/CreateSpawnableRequest.cs b/Models/DTOs/CreateSpawnableRequest.cs new file mode 100644 index 0000000..862ac0b --- /dev/null +++ b/Models/DTOs/CreateSpawnableRequest.cs @@ -0,0 +1,7 @@ +namespace GameAuthApi.Models; + +public class CreateSpawnableRequest +{ + public string Name { get; set; } + public int SpawnRate { get; set; } +} diff --git a/Models/DTOs/LoginRequest.cs b/Models/DTOs/LoginRequest.cs new file mode 100644 index 0000000..74fac02 --- /dev/null +++ b/Models/DTOs/LoginRequest.cs @@ -0,0 +1,7 @@ +namespace GameAuthApi.Models; + +public class LoginRequest +{ + public string Username { get; set; } + public string Password { get; set; } +} diff --git a/Models/DTOs/MoveRequest.cs b/Models/DTOs/MoveRequest.cs new file mode 100644 index 0000000..d86d10d --- /dev/null +++ b/Models/DTOs/MoveRequest.cs @@ -0,0 +1,6 @@ +namespace GameAuthApi.Models; + +public class MoveRequest +{ + public string Direction { get; set; } // N, NE, E, SE, S, SW, W, NW +} diff --git a/Models/DTOs/RegisterRequest.cs b/Models/DTOs/RegisterRequest.cs new file mode 100644 index 0000000..73df6b9 --- /dev/null +++ b/Models/DTOs/RegisterRequest.cs @@ -0,0 +1,8 @@ +namespace GameAuthApi.Models; + +public class RegisterRequest +{ + public string Username { get; set; } + public string Password { get; set; } + public string Email { get; set; } +} \ No newline at end of file diff --git a/Models/DTOs/TeleportRequest.cs b/Models/DTOs/TeleportRequest.cs new file mode 100644 index 0000000..4ccfeb9 --- /dev/null +++ b/Models/DTOs/TeleportRequest.cs @@ -0,0 +1,7 @@ +namespace GameAuthApi.Models; + +public class TeleportRequest +{ + public string PlayerId { get; set; } + public string LocationId { get; set; } +} \ No newline at end of file diff --git a/Models/Item.cs b/Models/Item.cs new file mode 100644 index 0000000..662ec28 --- /dev/null +++ b/Models/Item.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace GameAuthApi.Models; + +public class Item +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public string LocationId { get; set; } + + public string Name { get; set; } +} diff --git a/Models/Location.cs b/Models/Location.cs new file mode 100644 index 0000000..0335aa9 --- /dev/null +++ b/Models/Location.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace GameAuthApi.Models; + +public class Location +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string LocationType { get; set; } = "World"; + + public int? X { get; set; } + public int? Y { get; set; } + public int? Z { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public string N { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string NE { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string E { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string SE { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string S { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string SW { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string W { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string NW { get; set; } + + public List Players { get; set; } = []; + + [BsonRepresentation(BsonType.ObjectId)] + public List Content { get; set; } = []; +} diff --git a/Models/Player.cs b/Models/Player.cs new file mode 100644 index 0000000..670536d --- /dev/null +++ b/Models/Player.cs @@ -0,0 +1,18 @@ +// Models/Player.cs +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace GameAuthApi.Models; + +public class Player +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string UserId { get; set; } // Linked to user + public string Name { get; set; } // In-game player name + public string LocationId { get; set; } // Current location (nullable) + public string InventoryLocationId { get; set; } // Inventory location (nullable) + public int Climbing { get; set; } = 3; // Climbing skill level (default 3) +} diff --git a/Models/Spawnable.cs b/Models/Spawnable.cs new file mode 100644 index 0000000..fe64ff6 --- /dev/null +++ b/Models/Spawnable.cs @@ -0,0 +1,14 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace GameAuthApi.Models; + +public class Spawnable +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string Name { get; set; } + public int SpawnRate { get; set; } +} diff --git a/Models/User.cs b/Models/User.cs new file mode 100644 index 0000000..2fec0d0 --- /dev/null +++ b/Models/User.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace GameAuthApi.Models; + +public class User +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string Username { get; set; } + public string PasswordHash { get; set; } + public string Role { get; set; } = "player"; + public string RefreshToken { get; set; } + public DateTime RefreshTokenExpiry { get; set; } + public string Email { get; set; } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..3765fdb --- /dev/null +++ b/Program.cs @@ -0,0 +1,83 @@ +using GameAuthApi.Services; +using GameAuthApi.Middleware; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { Title = "GameAuthApi", Version = "v1" }); + + c.AddSecurityDefinition("Bearer", new() + { + Name = "Authorization", + Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Description = "Enter: `Bearer `" + }); + + c.AddSecurityRequirement(new() + { + { + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); +}); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var jwtKey = builder.Configuration["Jwt:Key"]; +var keyBytes = Encoding.UTF8.GetBytes(jwtKey); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Issuer"], + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(keyBytes) + }; + }); + +var app = builder.Build(); + +app.UseSwagger(); +app.UseSwaggerUI(); + +app.UseMiddleware(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/Services/BlacklistService.cs b/Services/BlacklistService.cs new file mode 100644 index 0000000..ec1803a --- /dev/null +++ b/Services/BlacklistService.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using GameAuthApi.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace GameAuthApi.Services; + +public class BlacklistService +{ + private readonly IMongoCollection _blacklist; + + public BlacklistService(IConfiguration config) + { + var client = new MongoClient(config["MongoDB:ConnectionString"]); + var db = client.GetDatabase(config["MongoDB:DatabaseName"]); + _blacklist = db.GetCollection("BlacklistedTokens"); + } + + public async Task AddToBlacklistAsync(string token, DateTime expiry) + { + var entry = new BlacklistedToken { Token = token, Expiry = expiry }; + await _blacklist.InsertOneAsync(entry); + } + + public async Task IsTokenBlacklistedAsync(string token) + { + var exists = await _blacklist.Find(x => x.Token == token && x.Expiry > DateTime.UtcNow).FirstOrDefaultAsync(); + return exists != null; + } +} \ No newline at end of file diff --git a/Services/ItemService.cs b/Services/ItemService.cs new file mode 100644 index 0000000..2381cfb --- /dev/null +++ b/Services/ItemService.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GameAuthApi.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace GameAuthApi.Services; + +public class ItemService +{ + private readonly IMongoCollection _items; + + public ItemService(IConfiguration config) + { + var client = new MongoClient(config["MongoDB:ConnectionString"]); + var db = client.GetDatabase(config["MongoDB:DatabaseName"]); + _items = db.GetCollection("Items"); + } + + public async Task CreateAsync(Item item) + { + await _items.InsertOneAsync(item); + } + + public async Task> GetByLocationAsync(string locationId) + { + return await _items.Find(i => i.LocationId == locationId).ToListAsync(); + } + + public async Task GetByIdAsync(string id) + { + return await _items.Find(i => i.Id == id).FirstOrDefaultAsync(); + } + + public async Task UpdateAsync(Item item) + { + await _items.ReplaceOneAsync(i => i.Id == item.Id, item); + } +} diff --git a/Services/LocationService.cs b/Services/LocationService.cs new file mode 100644 index 0000000..72a556c --- /dev/null +++ b/Services/LocationService.cs @@ -0,0 +1,69 @@ +using System.Threading.Tasks; +using GameAuthApi.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace GameAuthApi.Services; + +public class LocationService +{ + private readonly IMongoCollection _locations; + + public LocationService(IConfiguration config) + { + var client = new MongoClient(config["MongoDB:ConnectionString"]); + var db = client.GetDatabase(config["MongoDB:DatabaseName"]); + _locations = db.GetCollection("Locations"); + } + + public async Task GetByIdAsync(string id) + { + if (!ObjectId.TryParse(id, out _)) + return null; // prevent crash if id is invalid + + return await _locations.Find(x => x.Id == id).FirstOrDefaultAsync(); + } + + public async Task CreateAsync(Location loc) => + await _locations.InsertOneAsync(loc); + + public async Task DeleteAsync(string id) => + await _locations.DeleteOneAsync(loc => loc.Id == id); + public async Task UpdateAsync(Location loc) => + await _locations.ReplaceOneAsync(l => l.Id == loc.Id, loc); + + public async Task AddPlayerToLocationAsync(string locationId, string playerId) + { + var update = Builders.Update.AddToSet(l => l.Players, playerId); + await _locations.UpdateOneAsync(l => l.Id == locationId, update); + } + + public async Task GetByCoordinatesAsync(int x, int y, int z) + { + return await _locations.Find(loc => loc.X == x && loc.Y == y && loc.Z == z).FirstOrDefaultAsync(); + } + + public async Task GetSpawnLocationAsync() + { + return await _locations.Find(loc => loc.X == 0 && loc.Y == 0).FirstOrDefaultAsync(); + } + + public async Task RemovePlayerFromLocationAsync(string locationId, string playerId) + { + var update = Builders.Update.Pull(l => l.Players, playerId); + await _locations.UpdateOneAsync(l => l.Id == locationId, update); + } + + public async Task AddItemToLocationAsync(string locationId, string itemId) + { + var update = Builders.Update.Push(l => l.Content, itemId); + await _locations.UpdateOneAsync(l => l.Id == locationId, update); + } + + public async Task RemoveItemFromLocationAsync(string locationId, string itemId) + { + var update = Builders.Update.Pull(l => l.Content, itemId); + await _locations.UpdateOneAsync(l => l.Id == locationId, update); + } +} diff --git a/Services/PlayerService.cs b/Services/PlayerService.cs new file mode 100644 index 0000000..e750a63 --- /dev/null +++ b/Services/PlayerService.cs @@ -0,0 +1,44 @@ +// Services/PlayerService.cs +using System.Collections.Generic; +using System.Threading.Tasks; +using GameAuthApi.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace GameAuthApi.Services; + +public class PlayerService +{ + private readonly IMongoCollection _players; + + public PlayerService(IConfiguration config) + { + var client = new MongoClient(config["MongoDB:ConnectionString"]); + var db = client.GetDatabase(config["MongoDB:DatabaseName"]); + _players = db.GetCollection("Players"); + } + + public async Task GetByUserIdAsync(string userId) => + await _players.Find(p => p.UserId == userId).FirstOrDefaultAsync(); + + public async Task> GetByLocationIdAsync(string locationId) + { + return await _players.Find(p => p.LocationId == locationId).ToListAsync(); + } + + public async Task GetByIdAsync(string id) => + await _players.Find(p => p.Id == id).FirstOrDefaultAsync(); + + public async Task CreateAsync(Player player) => + await _players.InsertOneAsync(player); + + public async Task UpdateAsync(Player player) + { + await _players.ReplaceOneAsync(p => p.Id == player.Id, player); + } + + public async Task DeleteAsync(string id) + { + await _players.DeleteOneAsync(p => p.Id == id); + } +} diff --git a/Services/SpawnableService.cs b/Services/SpawnableService.cs new file mode 100644 index 0000000..0b545c7 --- /dev/null +++ b/Services/SpawnableService.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GameAuthApi.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace GameAuthApi.Services; + +public class SpawnableService +{ + private readonly IMongoCollection _spawnables; + + public SpawnableService(IConfiguration config) + { + var client = new MongoClient(config["MongoDB:ConnectionString"]); + var db = client.GetDatabase(config["MongoDB:DatabaseName"]); + _spawnables = db.GetCollection("Spawnables"); + } + + public async Task CreateAsync(Spawnable spawnable) + { + await _spawnables.InsertOneAsync(spawnable); + } + + public async Task> GetAllAsync() + { + return await _spawnables.Find(_ => true).ToListAsync(); + } +} diff --git a/Services/UserService.cs b/Services/UserService.cs new file mode 100644 index 0000000..14c63f8 --- /dev/null +++ b/Services/UserService.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using GameAuthApi.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace GameAuthApi.Services; + +public class UserService +{ + private readonly IMongoCollection _users; + + public UserService(IConfiguration config) + { + var client = new MongoClient(config["MongoDB:ConnectionString"]); + var database = client.GetDatabase(config["MongoDB:DatabaseName"]); + _users = database.GetCollection("Users"); + } + + public async Task GetByUsernameAsync(string username) => + await _users.Find(u => u.Username == username).FirstOrDefaultAsync(); + + public async Task CreateAsync(User user) => + await _users.InsertOneAsync(user); + + public async Task UpdateAsync(User user) => + await _users.ReplaceOneAsync(u => u.Id == user.Id, user); +} \ No newline at end of file diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..d8e9325 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,16 @@ +{ + "MongoDB": { + "ConnectionString": "mongodb://192.168.86.50:27017", + "DatabaseName": "GameDb" + }, + "Jwt": { + "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", + "Issuer": "GameAuthApi" + }, + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file