mirror of
https://github.com/Wessel/nhl-setgame.git
synced 2026-07-25 17:51:35 +02:00
feat(backend): Seperate database activity from controllers
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActionResult<IEnumerable<Game>>> 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<ActionResult<Game>> 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<ActionResult<Game>> PostGame() {
|
||||
var user = ParseUserId();
|
||||
if (user == null) {
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var newGame = new Game {
|
||||
UserId = user.Value,
|
||||
Deck = [..
|
||||
from shape in Enum.GetValues<CardShape>().Cast<CardShape>()
|
||||
from color in Enum.GetValues<CardColor>().Cast<CardColor>()
|
||||
from count in Enum.GetValues<CardCount>().Cast<CardCount>()
|
||||
from shade in Enum.GetValues<CardShade>().Cast<CardShade>()
|
||||
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<IActionResult> 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<ActionResult<Game>> 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<IActionResult> 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<ActionResult<List<int[]>>> 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<ActionResult<List<ushort>>> 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;
|
||||
}
|
||||
}
|
||||
13
backend/backend/Interfaces/IAuthService.cs
Normal file
13
backend/backend/Interfaces/IAuthService.cs
Normal file
@@ -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);
|
||||
}
|
||||
11
backend/backend/Interfaces/IGameRepository.cs
Normal file
11
backend/backend/Interfaces/IGameRepository.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Backend.Models;
|
||||
|
||||
namespace Backend.Interfaces;
|
||||
|
||||
public interface IGameRepository {
|
||||
Task<IEnumerable<Game>> GetGamesByUserIdAsync(long userId);
|
||||
Task<Game?> GetGameByIdAsync(long id, long userId);
|
||||
Task<Game> CreateGameAsync(Game game);
|
||||
Task<Game> UpdateGameAsync(Game game);
|
||||
Task DeleteGameAsync(Game game);
|
||||
}
|
||||
12
backend/backend/Interfaces/IGameService.cs
Normal file
12
backend/backend/Interfaces/IGameService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Backend.Models;
|
||||
|
||||
namespace Backend.Interfaces;
|
||||
public interface IGameService {
|
||||
Task<IEnumerable<Game>> GetPlayableGamesAsync(long userId);
|
||||
Task<Game?> GetGameAsync(long gameId, long userId);
|
||||
Task<Game> CreateGameAsync(long userId);
|
||||
Task<bool> DeleteGameAsync(long gameId, long userId);
|
||||
Task<SetCheckResult?> CheckSetAsync(long gameId, long userId, ushort[] cardIndices);
|
||||
Task<List<int[]>> GetSetsInHandAsync(long gameId, long userId);
|
||||
Task<List<ushort>> GetHintAsync(long gameId, long userId);
|
||||
}
|
||||
9
backend/backend/Interfaces/IUserRepository.cs
Normal file
9
backend/backend/Interfaces/IUserRepository.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Backend.Models;
|
||||
|
||||
namespace Backend.Interfaces;
|
||||
|
||||
public interface IUserRepository {
|
||||
User? GetUserByUsername(string username);
|
||||
User? ValidateUser(string username, string passwordHash);
|
||||
Task<User> CreateUserAsync(User user);
|
||||
}
|
||||
@@ -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<long>("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<long>("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");
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace backend.Migrations
|
||||
namespace Backend.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialDatabase : Migration
|
||||
|
||||
@@ -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<long>("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<long>("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");
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace backend.Models;
|
||||
namespace Backend.Models;
|
||||
|
||||
public struct SetCheckResult {
|
||||
public bool? IsSet { get; set; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace backend.Models;
|
||||
namespace Backend.Models;
|
||||
|
||||
public class GameContext : DbContext
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace backend.Models;
|
||||
namespace Backend.Models;
|
||||
|
||||
[Index(nameof(Username), IsUnique = true)]
|
||||
public class User {
|
||||
|
||||
@@ -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<GameContext>(opt => opt.UseNpgsql(postgres_connection_string));
|
||||
|
||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||
builder.Services.AddScoped<IGameRepository, GameRepository>();
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<IGameService, GameService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Disable CORS
|
||||
|
||||
46
backend/backend/Repositories/GameRepository.cs
Normal file
46
backend/backend/Repositories/GameRepository.cs
Normal file
@@ -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<IEnumerable<Game>> GetGamesByUserIdAsync(long userId) {
|
||||
return await _context.Games
|
||||
.Where(g => g.UserId == userId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Game?> GetGameByIdAsync(long id, long userId) {
|
||||
return await _context.Games
|
||||
.Where(g => g.Id == id && g.UserId == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Game> CreateGameAsync(Game game) {
|
||||
_context.Games.Add(game);
|
||||
await _context.SaveChangesAsync();
|
||||
return game;
|
||||
}
|
||||
|
||||
public async Task<Game> 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();
|
||||
}
|
||||
}
|
||||
23
backend/backend/Repositories/UserRepository.cs
Normal file
23
backend/backend/Repositories/UserRepository.cs
Normal file
@@ -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<User> CreateUserAsync(User user) {
|
||||
_context.Users.Add(user);
|
||||
await _context.SaveChangesAsync();
|
||||
return user;
|
||||
}
|
||||
}
|
||||
46
backend/backend/Services/AuthService.cs
Normal file
46
backend/backend/Services/AuthService.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
95
backend/backend/Services/GameService.cs
Normal file
95
backend/backend/Services/GameService.cs
Normal file
@@ -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<IEnumerable<Game>> GetPlayableGamesAsync(long userId) {
|
||||
return await _gameRepository.GetGamesByUserIdAsync(userId);
|
||||
}
|
||||
|
||||
public async Task<Game?> GetGameAsync(long gameId, long userId) {
|
||||
return await _gameRepository.GetGameByIdAsync(gameId, userId);
|
||||
}
|
||||
|
||||
public async Task<Game> CreateGameAsync(long userId) {
|
||||
var newGame = new Game {
|
||||
UserId = userId,
|
||||
StartedAt = DateTime.UtcNow,
|
||||
Deck = [..
|
||||
from shape in Enum.GetValues<CardShape>().Cast<CardShape>()
|
||||
from color in Enum.GetValues<CardColor>().Cast<CardColor>()
|
||||
from count in Enum.GetValues<CardCount>().Cast<CardCount>()
|
||||
from shade in Enum.GetValues<CardShade>().Cast<CardShade>()
|
||||
select new Card {
|
||||
Shape = shape,
|
||||
Color = color,
|
||||
Count = count,
|
||||
Shade = shade
|
||||
}.ToUshort()
|
||||
]
|
||||
};
|
||||
|
||||
newGame.ShuffleDeck();
|
||||
newGame.DealHand();
|
||||
|
||||
return await _gameRepository.CreateGameAsync(newGame);
|
||||
}
|
||||
|
||||
public async Task<bool> 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<SetCheckResult?> 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<List<int[]>> GetSetsInHandAsync(long gameId, long userId) {
|
||||
var game = await _gameRepository.GetGameByIdAsync(gameId, userId);
|
||||
|
||||
if (game == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return game.GetIndicesOfSet();
|
||||
}
|
||||
|
||||
public async Task<List<ushort>> 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)];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user