From 8a919c7cda9c3d1d66dd2803a5ae1b44f9a0a79d Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Mon, 31 Mar 2025 20:16:44 +0000 Subject: [PATCH] feat(backend): Seperate database activity from controllers --- backend/backend/Controllers/AuthController.cs | 60 +---- .../backend/Controllers/GamesController.cs | 212 ++++++------------ backend/backend/Interfaces/IAuthService.cs | 13 ++ backend/backend/Interfaces/IGameRepository.cs | 11 + backend/backend/Interfaces/IGameService.cs | 12 + backend/backend/Interfaces/IUserRepository.cs | 9 + ...20250325145201_InitialDatabase.Designer.cs | 14 +- .../20250325145201_InitialDatabase.cs | 2 +- .../Migrations/GameContextModelSnapshot.cs | 14 +- backend/backend/Models/Card.cs | 2 +- backend/backend/Models/Game.cs | 2 +- backend/backend/Models/GameContext.cs | 2 +- backend/backend/Models/User.cs | 2 +- backend/backend/Program.cs | 10 +- .../backend/Repositories/GameRepository.cs | 46 ++++ .../backend/Repositories/UserRepository.cs | 23 ++ backend/backend/Services/AuthService.cs | 46 ++++ backend/backend/Services/GameService.cs | 95 ++++++++ .../src/app/game-list/game-list.component.ts | 3 +- .../login-screen/login-screen.component.ts | 2 +- .../environments/environment.development.ts | 7 +- frontend/src/service/auth/auth.guard.ts | 9 +- 22 files changed, 376 insertions(+), 220 deletions(-) create mode 100644 backend/backend/Interfaces/IAuthService.cs create mode 100644 backend/backend/Interfaces/IGameRepository.cs create mode 100644 backend/backend/Interfaces/IGameService.cs create mode 100644 backend/backend/Interfaces/IUserRepository.cs create mode 100644 backend/backend/Repositories/GameRepository.cs create mode 100644 backend/backend/Repositories/UserRepository.cs create mode 100644 backend/backend/Services/AuthService.cs create mode 100644 backend/backend/Services/GameService.cs diff --git a/backend/backend/Controllers/AuthController.cs b/backend/backend/Controllers/AuthController.cs index 994913b..3a98e82 100644 --- a/backend/backend/Controllers/AuthController.cs +++ b/backend/backend/Controllers/AuthController.cs @@ -1,66 +1,22 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.Tokens; -using backend.Models; +using Backend.Interfaces; -namespace backend.Controllers; - -public class UserLogin { - public required string Username { get; set; } - public required string Password { get; set; } -} +namespace Backend.Controllers; [ApiController] [Route("api/v1/[controller]")] -public class AuthController(GameContext context) : ControllerBase { - private readonly GameContext _context = context; +public class AuthController(IAuthService authService) : ControllerBase { + private readonly IAuthService _authService = authService; [HttpPost("login")] - public IActionResult Login([FromBody] UserLogin loginData) { - var salt = Environment.GetEnvironmentVariable("MD5_SALT") ?? ""; - var passwordHash = CreateMD5(loginData.Password + salt); - - var user = _context - .Users - .Where(u => u.Username == loginData.Username && u.PasswordHash == passwordHash) - .FirstOrDefault(); + public IActionResult Login([FromBody] UserLogin credentials) { + var user = _authService.ValidateUser(credentials); if (user != null) { - var token = GenerateJwtToken(user.Id.ToString()); - + var token = _authService.GenerateJwtToken(user.Id.ToString()); return Ok(new { token }); } return Unauthorized(); } - - private static string GenerateJwtToken(string userId) { - var secret = Environment.GetEnvironmentVariable("JWT_SECRET") ?? ""; - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); - var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - - var claims = new[] { - new Claim(JwtRegisteredClaimNames.Sub, userId), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }; - - var token = new JwtSecurityToken( - signingCredentials: creds, - issuer: Environment.GetEnvironmentVariable("JWT_ISSUER") ?? "", - audience: Environment.GetEnvironmentVariable("JWT_AUDIENCE") ?? "", - claims: claims, - expires: DateTime.Now.AddHours(6) - ); - - return new JwtSecurityTokenHandler().WriteToken(token); - } - - public static string CreateMD5(string input) { - byte[] inputBytes = Encoding.ASCII.GetBytes(input); - byte[] hashBytes = System.Security.Cryptography.MD5.HashData(inputBytes); - - return Convert.ToHexString(hashBytes); - } -} +} \ No newline at end of file diff --git a/backend/backend/Controllers/GamesController.cs b/backend/backend/Controllers/GamesController.cs index 0d8d5b5..f88ac43 100755 --- a/backend/backend/Controllers/GamesController.cs +++ b/backend/backend/Controllers/GamesController.cs @@ -1,119 +1,84 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using backend.Models; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; -using DotNetEnv; +using Backend.Models; +using Backend.Interfaces; namespace backend.Controllers; -[Route("/api/v1/[controller]")] [ApiController] -public class GamesController(GameContext context) : ControllerBase { - private readonly GameContext _context = context; +[Route("api/v1/[controller]")] +public class GamesController(IGameService gameService) : ControllerBase { + private readonly IGameService _gameService = gameService; private long? ParseUserId() { var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - - if (long.TryParse(userIdClaim, out var userId)) { - return userId; - } - - return null; + return long.TryParse(userIdClaim, out var userId) ? userId : null; } - // GET: api/Games + // GET: api/v1/Games [HttpGet] [Authorize] public async Task>> GetGames() { - var user = ParseUserId(); - if (user == null) { + var userId = ParseUserId(); + if (userId == null) { return BadRequest(); } - return await _context.Games - .Where(g => g.UserId == user) - .ToListAsync(); + var games = await _gameService.GetPlayableGamesAsync(userId.Value); + return Ok(games); } - // GET: api/Games/5 + // GET: api/v1/Games/5 [HttpGet("{id}")] [Authorize] public async Task> GetGame(long id) { - var user = ParseUserId(); - if (user == null) { - return BadRequest(); + var userId = ParseUserId(); + if (userId == null) { + return BadRequest("Invalid user ID"); } - var game = await _context.Games - .Where(g => g.Id == id && g.UserId == user) - .FirstOrDefaultAsync(); - - if (game == null || game.UserId != user) { - return NotFound(); - } - - return game; - } - - // POST: api/Games - // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 - [HttpPost] - [Authorize] - public async Task> PostGame() { - var user = ParseUserId(); - if (user == null) { - return BadRequest(); - } - - var newGame = new Game { - UserId = user.Value, - Deck = [.. - from shape in Enum.GetValues().Cast() - from color in Enum.GetValues().Cast() - from count in Enum.GetValues().Cast() - from shade in Enum.GetValues().Cast() - select new Card { - Shape = shape, - Color = color, - Count = count, - Shade = shade - }.ToUshort() - ] - }; - - newGame.ShuffleDeck(); - newGame.DealHand(); - - _context.Games.Add(newGame); - await _context.SaveChangesAsync(); - - return CreatedAtAction(nameof(GetGame), new { id = newGame.Id }, newGame); - } - - // DELETE: api/Games/5 - [HttpDelete("{id}")] - [Authorize] - public async Task DeleteGame(long id) { - var user = ParseUserId(); - if (user == null) { - return BadRequest(); - } - - var game = await _context.Games - .Where(g => g.Id == id && g.UserId == user) - .FirstOrDefaultAsync(); - + var game = await _gameService.GetGameAsync(id, userId.Value); + if (game == null) { return NotFound(); } - - _context.Games.Remove(game); - await _context.SaveChangesAsync(); - - return NoContent(); + + return game; } + // POST: api/v1/Games + [HttpPost] + [Authorize] + public async Task> PostGame() { + var userId = ParseUserId(); + if (userId == null) { + return BadRequest(); + } + + var game = await _gameService.CreateGameAsync(userId.Value); + return CreatedAtAction(nameof(GetGame), new { id = game.Id }, game); + } + + // DELETE: api/v1/Games/5 + [HttpDelete("{id}")] + [Authorize] + public async Task DeleteGame(long id) { + var userId = ParseUserId(); + if (userId == null) { + return BadRequest(); + } + + var result = await _gameService.DeleteGameAsync(id, userId.Value); + + if (!result) { + return NotFound(); + } + + return NoContent(); + } + + // POST: api/v1/Games/CheckSet/5 [HttpPost] [Route("[action]/{id}")] [Authorize] @@ -121,84 +86,55 @@ public class GamesController(GameContext context) : ControllerBase { long id, [FromBody] ushort[] cardIndices ) { - var user = ParseUserId(); - if (user == null) { + var userId = ParseUserId(); + if (userId == null) { return BadRequest(); } - var game = await _context.Games - .Where(g => g.Id == id && g.UserId == user) - .FirstOrDefaultAsync(); - - if (game == null) { - return NotFound(); - } - - var res = game.IsSet(cardIndices); - - if (res == null) { + var result = await _gameService.CheckSetAsync(id, userId.Value, cardIndices); + + if (result == null) { return BadRequest(); } - await _context.SaveChangesAsync(); - - return res; + return result; } + // GET: api/v1/Games/SetsInHand/5 [HttpGet] [Route("[action]/{id}")] [Authorize] public async Task>> SetsInHand(long id) { - // Only show if user is admin (id < 1) - var user = ParseUserId(); - if (user == null || user > 0) { + var userId = ParseUserId(); + if (userId == null) { return BadRequest(); } - var game = await _context.Games - .Where(g => g.Id == id && g.UserId == user) - .FirstOrDefaultAsync(); - - if (game == null) { + var result = await _gameService.GetSetsInHandAsync(id, userId.Value); + + if (result == null) { return NotFound(); } - var res = game.GetIndicesOfSet(); - - if (res == null) { - return BadRequest(); - } - - return res; + return result; } + // GET: api/v1/Games/Hint/5 [HttpGet] [Route("[action]/{id}")] [Authorize] public async Task>> Hint(long id) { - var user = ParseUserId(); - if (user == null) { + var userId = ParseUserId(); + if (userId == null) { + return BadRequest(); + } + + var result = await _gameService.GetHintAsync(id, userId.Value); + + if (result == null) { return BadRequest(); } - var game = await _context.Games - .Where(g => g.Id == id && g.UserId == user) - .FirstOrDefaultAsync(); - - if (game == null) { - return NotFound(); - } - - var sets = game.GetIndicesOfSet(); - - if (sets == null || sets.Count == 0) { - return BadRequest("No sets found in hand."); - } - - game.Hints++; - - await _context.SaveChangesAsync(); - - return sets[0].Take(2).Select(i => (ushort)i).ToList(); + return result; } } \ No newline at end of file diff --git a/backend/backend/Interfaces/IAuthService.cs b/backend/backend/Interfaces/IAuthService.cs new file mode 100644 index 0000000..f76d0c0 --- /dev/null +++ b/backend/backend/Interfaces/IAuthService.cs @@ -0,0 +1,13 @@ +using Backend.Models; + +namespace Backend.Interfaces; + +public class UserLogin { + public required string Username { get; set; } + public required string Password { get; set; } +} + +public interface IAuthService { + User? ValidateUser(UserLogin credentials); + string GenerateJwtToken(string userId); +} \ No newline at end of file diff --git a/backend/backend/Interfaces/IGameRepository.cs b/backend/backend/Interfaces/IGameRepository.cs new file mode 100644 index 0000000..c75567d --- /dev/null +++ b/backend/backend/Interfaces/IGameRepository.cs @@ -0,0 +1,11 @@ +using Backend.Models; + +namespace Backend.Interfaces; + +public interface IGameRepository { + Task> GetGamesByUserIdAsync(long userId); + Task GetGameByIdAsync(long id, long userId); + Task CreateGameAsync(Game game); + Task UpdateGameAsync(Game game); + Task DeleteGameAsync(Game game); +} \ No newline at end of file diff --git a/backend/backend/Interfaces/IGameService.cs b/backend/backend/Interfaces/IGameService.cs new file mode 100644 index 0000000..b872316 --- /dev/null +++ b/backend/backend/Interfaces/IGameService.cs @@ -0,0 +1,12 @@ +using Backend.Models; + +namespace Backend.Interfaces; +public interface IGameService { + Task> GetPlayableGamesAsync(long userId); + Task GetGameAsync(long gameId, long userId); + Task CreateGameAsync(long userId); + Task DeleteGameAsync(long gameId, long userId); + Task CheckSetAsync(long gameId, long userId, ushort[] cardIndices); + Task> GetSetsInHandAsync(long gameId, long userId); + Task> GetHintAsync(long gameId, long userId); +} \ No newline at end of file diff --git a/backend/backend/Interfaces/IUserRepository.cs b/backend/backend/Interfaces/IUserRepository.cs new file mode 100644 index 0000000..f724639 --- /dev/null +++ b/backend/backend/Interfaces/IUserRepository.cs @@ -0,0 +1,9 @@ +using Backend.Models; + +namespace Backend.Interfaces; + +public interface IUserRepository { + User? GetUserByUsername(string username); + User? ValidateUser(string username, string passwordHash); + Task CreateUserAsync(User user); +} diff --git a/backend/backend/Migrations/20250325145201_InitialDatabase.Designer.cs b/backend/backend/Migrations/20250325145201_InitialDatabase.Designer.cs index 5c3e7e6..fa08300 100644 --- a/backend/backend/Migrations/20250325145201_InitialDatabase.Designer.cs +++ b/backend/backend/Migrations/20250325145201_InitialDatabase.Designer.cs @@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using backend.Models; +using Backend.Models; #nullable disable -namespace backend.Migrations +namespace Backend.Migrations { [DbContext(typeof(GameContext))] [Migration("20250325145201_InitialDatabase")] @@ -25,7 +25,7 @@ namespace backend.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("backend.Models.Game", b => + modelBuilder.Entity("Backend.Models.Game", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -67,7 +67,7 @@ namespace backend.Migrations b.ToTable("Games"); }); - modelBuilder.Entity("backend.Models.User", b => + modelBuilder.Entity("Backend.Models.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -100,16 +100,16 @@ namespace backend.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("backend.Models.Game", b => + modelBuilder.Entity("Backend.Models.Game", b => { - b.HasOne("backend.Models.User", null) + b.HasOne("Backend.Models.User", null) .WithMany("Games") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("backend.Models.User", b => + modelBuilder.Entity("Backend.Models.User", b => { b.Navigation("Games"); }); diff --git a/backend/backend/Migrations/20250325145201_InitialDatabase.cs b/backend/backend/Migrations/20250325145201_InitialDatabase.cs index 724cc19..122efb3 100644 --- a/backend/backend/Migrations/20250325145201_InitialDatabase.cs +++ b/backend/backend/Migrations/20250325145201_InitialDatabase.cs @@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace backend.Migrations +namespace Backend.Migrations { /// public partial class InitialDatabase : Migration diff --git a/backend/backend/Migrations/GameContextModelSnapshot.cs b/backend/backend/Migrations/GameContextModelSnapshot.cs index cb09c84..d21581b 100644 --- a/backend/backend/Migrations/GameContextModelSnapshot.cs +++ b/backend/backend/Migrations/GameContextModelSnapshot.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using backend.Models; +using Backend.Models; #nullable disable -namespace backend.Migrations +namespace Backend.Migrations { [DbContext(typeof(GameContext))] partial class GameContextModelSnapshot : ModelSnapshot @@ -22,7 +22,7 @@ namespace backend.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("backend.Models.Game", b => + modelBuilder.Entity("Backend.Models.Game", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -64,7 +64,7 @@ namespace backend.Migrations b.ToTable("Games", (string)null); }); - modelBuilder.Entity("backend.Models.User", b => + modelBuilder.Entity("Backend.Models.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -97,16 +97,16 @@ namespace backend.Migrations b.ToTable("Users", (string)null); }); - modelBuilder.Entity("backend.Models.Game", b => + modelBuilder.Entity("Backend.Models.Game", b => { - b.HasOne("backend.Models.User", null) + b.HasOne("Backend.Models.User", null) .WithMany("Games") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("backend.Models.User", b => + modelBuilder.Entity("Backend.Models.User", b => { b.Navigation("Games"); }); diff --git a/backend/backend/Models/Card.cs b/backend/backend/Models/Card.cs index f4452c4..0788af9 100755 --- a/backend/backend/Models/Card.cs +++ b/backend/backend/Models/Card.cs @@ -1,4 +1,4 @@ -namespace backend.Models; +namespace Backend.Models; // 3, 7 and 8 are chosen due to them in no way, // shape or form adding up or substracting into eachother diff --git a/backend/backend/Models/Game.cs b/backend/backend/Models/Game.cs index c890246..0894af8 100755 --- a/backend/backend/Models/Game.cs +++ b/backend/backend/Models/Game.cs @@ -1,4 +1,4 @@ -namespace backend.Models; +namespace Backend.Models; public struct SetCheckResult { public bool? IsSet { get; set; } diff --git a/backend/backend/Models/GameContext.cs b/backend/backend/Models/GameContext.cs index b5c0901..24c4b1a 100755 --- a/backend/backend/Models/GameContext.cs +++ b/backend/backend/Models/GameContext.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace backend.Models; +namespace Backend.Models; public class GameContext : DbContext { diff --git a/backend/backend/Models/User.cs b/backend/backend/Models/User.cs index 0142021..a1d7fb6 100644 --- a/backend/backend/Models/User.cs +++ b/backend/backend/Models/User.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace backend.Models; +namespace Backend.Models; [Index(nameof(Username), IsUnique = true)] public class User { diff --git a/backend/backend/Program.cs b/backend/backend/Program.cs index 0d2852f..e9c188d 100755 --- a/backend/backend/Program.cs +++ b/backend/backend/Program.cs @@ -1,9 +1,12 @@ using Microsoft.EntityFrameworkCore; -using backend.Models; +using Backend.Models; using DotNetEnv; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; +using Backend.Interfaces; +using Backend.Services; +using Backend.Repositories; // Load .env file Env.Load(); @@ -42,6 +45,11 @@ var postgres_connection_string = builder.Services.AddDbContext(opt => opt.UseNpgsql(postgres_connection_string)); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + var app = builder.Build(); // Disable CORS diff --git a/backend/backend/Repositories/GameRepository.cs b/backend/backend/Repositories/GameRepository.cs new file mode 100644 index 0000000..d91ce33 --- /dev/null +++ b/backend/backend/Repositories/GameRepository.cs @@ -0,0 +1,46 @@ +using Backend.Models; +using Backend.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Repositories; + +public class GameRepository(GameContext context) : IGameRepository { + private readonly GameContext _context = context; + + public async Task> GetGamesByUserIdAsync(long userId) { + return await _context.Games + .Where(g => g.UserId == userId) + .ToListAsync(); + } + + public async Task GetGameByIdAsync(long id, long userId) { + return await _context.Games + .Where(g => g.Id == id && g.UserId == userId) + .FirstOrDefaultAsync(); + } + + public async Task CreateGameAsync(Game game) { + _context.Games.Add(game); + await _context.SaveChangesAsync(); + return game; + } + + public async Task UpdateGameAsync(Game game) { + _context.Entry(game).State = EntityState.Modified; + await _context.SaveChangesAsync(); + return game; + } + + public async Task DeleteGameAsync(long id) { + var game = await _context.Games.FindAsync(id); + if (game != null) { + _context.Games.Remove(game); + await _context.SaveChangesAsync(); + } + } + + public async Task DeleteGameAsync(Game game) { + _context.Games.Remove(game); + await _context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/backend/backend/Repositories/UserRepository.cs b/backend/backend/Repositories/UserRepository.cs new file mode 100644 index 0000000..e135a07 --- /dev/null +++ b/backend/backend/Repositories/UserRepository.cs @@ -0,0 +1,23 @@ +using Backend.Interfaces; +using Backend.Models; + +namespace Backend.Repositories; + +public class UserRepository(GameContext context) : IUserRepository { + private readonly GameContext _context = context; + + public User? GetUserByUsername(string username) { + return _context.Users.FirstOrDefault(u => u.Username == username); + } + + public User? ValidateUser(string username, string passwordHash) { + return _context.Users + .FirstOrDefault(u => u.Username == username && u.PasswordHash == passwordHash); + } + + public async Task CreateUserAsync(User user) { + _context.Users.Add(user); + await _context.SaveChangesAsync(); + return user; + } +} diff --git a/backend/backend/Services/AuthService.cs b/backend/backend/Services/AuthService.cs new file mode 100644 index 0000000..8cd4f79 --- /dev/null +++ b/backend/backend/Services/AuthService.cs @@ -0,0 +1,46 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Backend.Interfaces; +using Backend.Models; +using Microsoft.IdentityModel.Tokens; + +namespace Backend.Services; + +public class AuthService(IUserRepository userRepository) : IAuthService { + private readonly IUserRepository _userRepository = userRepository; + + public User? ValidateUser(UserLogin credentials) { + var salt = Environment.GetEnvironmentVariable("MD5_SALT") ?? ""; + var passwordHash = CreateMD5(credentials.Password + salt); + + return _userRepository.ValidateUser(credentials.Username, passwordHash); + } + + public string GenerateJwtToken(string userId) { + var secret = Environment.GetEnvironmentVariable("JWT_SECRET") ?? ""; + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] { + new Claim(JwtRegisteredClaimNames.Sub, userId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var token = new JwtSecurityToken( + signingCredentials: creds, + issuer: Environment.GetEnvironmentVariable("JWT_ISSUER") ?? "", + audience: Environment.GetEnvironmentVariable("JWT_AUDIENCE") ?? "", + claims: claims, + expires: DateTime.Now.AddHours(6) + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + private static string CreateMD5(string input) { + byte[] inputBytes = Encoding.ASCII.GetBytes(input); + byte[] hashBytes = System.Security.Cryptography.MD5.HashData(inputBytes); + return Convert.ToHexString(hashBytes); + } +} diff --git a/backend/backend/Services/GameService.cs b/backend/backend/Services/GameService.cs new file mode 100644 index 0000000..5c6e6f7 --- /dev/null +++ b/backend/backend/Services/GameService.cs @@ -0,0 +1,95 @@ +using Backend.Models; +using Backend.Interfaces; + +namespace Backend.Services; +public class GameService(IGameRepository gameRepository) : IGameService { + private readonly IGameRepository _gameRepository = gameRepository; + + public async Task> GetPlayableGamesAsync(long userId) { + return await _gameRepository.GetGamesByUserIdAsync(userId); + } + + public async Task GetGameAsync(long gameId, long userId) { + return await _gameRepository.GetGameByIdAsync(gameId, userId); + } + + public async Task CreateGameAsync(long userId) { + var newGame = new Game { + UserId = userId, + StartedAt = DateTime.UtcNow, + Deck = [.. + from shape in Enum.GetValues().Cast() + from color in Enum.GetValues().Cast() + from count in Enum.GetValues().Cast() + from shade in Enum.GetValues().Cast() + select new Card { + Shape = shape, + Color = color, + Count = count, + Shade = shade + }.ToUshort() + ] + }; + + newGame.ShuffleDeck(); + newGame.DealHand(); + + return await _gameRepository.CreateGameAsync(newGame); + } + + public async Task DeleteGameAsync(long gameId, long userId) { + var game = await _gameRepository.GetGameByIdAsync(gameId, userId); + + if (game == null) { + return false; + } + + await _gameRepository.DeleteGameAsync(game); + return true; + } + + public async Task CheckSetAsync(long gameId, long userId, ushort[] cardIndices) { + var game = await _gameRepository.GetGameByIdAsync(gameId, userId); + + if (game == null) { + return null; + } + + var result = game.IsSet(cardIndices); + + if (result != null) { + await _gameRepository.UpdateGameAsync(game); + } + + return result; + } + + public async Task> GetSetsInHandAsync(long gameId, long userId) { + var game = await _gameRepository.GetGameByIdAsync(gameId, userId); + + if (game == null) { + return []; + } + + return game.GetIndicesOfSet(); + } + + public async Task> GetHintAsync(long gameId, long userId) { + var game = await _gameRepository.GetGameByIdAsync(gameId, userId); + + if (game == null) { + return []; + } + + var sets = game.GetIndicesOfSet(); + + if (sets == null || sets.Count == 0) { + return []; + } + + game.Hints++; + await _gameRepository.UpdateGameAsync(game); + + return [.. sets[0].Take(2).Select(i => (ushort)i)]; + } +} \ No newline at end of file diff --git a/frontend/src/app/game-list/game-list.component.ts b/frontend/src/app/game-list/game-list.component.ts index ff7250a..f786d93 100644 --- a/frontend/src/app/game-list/game-list.component.ts +++ b/frontend/src/app/game-list/game-list.component.ts @@ -2,6 +2,7 @@ import { Router } from '@angular/router'; import { Component, inject } from '@angular/core'; import { UserDataService } from '../../service/user/user-data.service'; import { CommonModule } from '@angular/common'; +import { GameResponse } from '../../service/game/game.types'; @Component({ selector: 'app-game-list', @@ -10,7 +11,7 @@ import { CommonModule } from '@angular/common'; styleUrl: './game-list.component.scss' }) export class GameListComponent { - games: { id: number, startedAt: Date, finishedAt: Date | null, fails: number, hand: any[], deck: any[] }[] | undefined; + games: GameResponse[] | undefined; userService: UserDataService = inject(UserDataService); router: Router = inject(Router); diff --git a/frontend/src/app/login-screen/login-screen.component.ts b/frontend/src/app/login-screen/login-screen.component.ts index ec67ff1..8a345c1 100644 --- a/frontend/src/app/login-screen/login-screen.component.ts +++ b/frontend/src/app/login-screen/login-screen.component.ts @@ -38,7 +38,7 @@ export class LoginScreenComponent { return; } - this.authService.login(this.username, this.password) + this.authService.login({ username: this.username, password: this.password }) .subscribe({ next: () => { this.router.navigate(['/']); diff --git a/frontend/src/environments/environment.development.ts b/frontend/src/environments/environment.development.ts index f274e5e..05ee054 100644 --- a/frontend/src/environments/environment.development.ts +++ b/frontend/src/environments/environment.development.ts @@ -1 +1,6 @@ -export const environment = {}; +export const environment = { + production: false, + apiUrl: 'http://localhost:5224/api/v1/Games', + authUrl: 'http://localhost:5224/api/v1/Auth', + authTokenKey: 'auth_token', +}; diff --git a/frontend/src/service/auth/auth.guard.ts b/frontend/src/service/auth/auth.guard.ts index 1b057a9..8a1690e 100644 --- a/frontend/src/service/auth/auth.guard.ts +++ b/frontend/src/service/auth/auth.guard.ts @@ -9,17 +9,12 @@ import { AuthService } from './auth.service'; export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot - ): Observable | Promise | boolean { + canActivate(): Observable | Promise | boolean { return this.authService.isAuthenticated().pipe( take(1), map(isAuthenticated => { if (!isAuthenticated) { - // Store the attempted URL for redirecting after login - this.authService.redirectUrl = state.url; - + // Navigate to the login page this.router.navigate(['/login']); return false;