chore(backend): Clean up Controllers

This commit is contained in:
2025-03-25 15:52:35 +01:00
parent 60f54ab770
commit f5a131ec4e
9 changed files with 340 additions and 339 deletions

View File

@@ -5,73 +5,62 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using backend.Models;
public class UserLogin
{
public string Username { get; set; }
public string Password { get; set; }
}
namespace backend.Controllers;
public class UserLogin {
public required string Username { get; set; }
public required string Password { get; set; }
}
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly GameContext _context;
[Route("api/v1/[controller]")]
public class AuthController(GameContext context) : ControllerBase {
private readonly GameContext _context = context;
public static string CreateMD5(string input)
{
// Use input string to calculate MD5 hash
using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
{
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes);
[HttpPost("login")]
public IActionResult Login([FromBody] UserLogin loginData) {
var salt = Environment.GetEnvironmentVariable("MD5_SALT") ?? "";
var passwordHash = CreateMD5(loginData.Password + salt);
return Convert.ToHexString(hashBytes); // .NET 5 +
var user = _context
.Users
.Where(u => u.Username == loginData.Username && u.PasswordHash == passwordHash)
.FirstOrDefault();
// Convert the byte array to hexadecimal string prior to .NET 5
// StringBuilder sb = new System.Text.StringBuilder();
// for (int i = 0; i < hashBytes.Length; i++)
// {
// sb.Append(hashBytes[i].ToString("X2"));
// }
// return sb.ToString();
}
}
public AuthController(GameContext context) {
_context = context;
}
[HttpPost("login")]
public IActionResult Login([FromBody] UserLogin user) {
var salt = Environment.GetEnvironmentVariable("MD5_SALT") ?? "";
var passwordHash = CreateMD5(user.Password + salt);
var dbUser = _context.Users.Where(u => u.Username == user.Username && u.PasswordHash == passwordHash).FirstOrDefault();
if (dbUser != null) {
var token = GenerateJwtToken(dbUser.Id.ToString());
return Ok(new { token });
}
return Unauthorized();
}
private string GenerateJwtToken(string userId)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("JWT_SECRET")));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "wessel.gg",
audience: "wessel.gg",
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
if (user != null) {
var token = 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);
}
}

View File

@@ -1,178 +1,172 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using backend.Models;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
namespace backend.Controllers
{
[Route("/api/v1/[controller]")]
[ApiController]
public class GamesController : ControllerBase {
private readonly GameContext _context;
namespace backend.Controllers;
public GamesController(GameContext context) {
_context = context;
[Route("/api/v1/[controller]")]
[ApiController]
public class GamesController(GameContext context) : ControllerBase {
private readonly GameContext _context = context;
private long? ParseUserId() {
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (long.TryParse(userIdClaim, out var userId)) {
return userId;
}
// GET: api/Games
[HttpGet]
[Authorize]
public async Task<ActionResult<IEnumerable<Game>>> GetGames() {
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return null;
}
if (!long.TryParse(userIdClaim, out var userId)) {
return Unauthorized();
}
// return await _context.Games.ToListAsync();
return await _context.Games.Where(g => g.UserId == userId).ToListAsync();
// GET: api/Games
[HttpGet]
[Authorize]
public async Task<ActionResult<IEnumerable<Game>>> GetGames() {
var user = ParseUserId();
if (user == null) {
return BadRequest();
}
// GET: api/Games/5
[HttpGet("{id}")]
[Authorize]
public async Task<ActionResult<Game>> GetGame(long id)
{
var game = await _context.Games.FindAsync(id);
return await _context.Games
.Where(g => g.UserId == user)
.ToListAsync();
}
if (game == null)
{
return NotFound();
}
return game;
// GET: api/Games/5
[HttpGet("{id}")]
[Authorize]
public async Task<ActionResult<Game>> GetGame(long id) {
var user = ParseUserId();
if (user == null) {
return BadRequest();
}
// PUT: api/Games/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
[Authorize]
public async Task<IActionResult> PutGame(long id, Game game)
{
if (id != game.Id)
{
return BadRequest();
}
var game = await _context.Games
.Where(g => g.Id == id && g.UserId == user)
.FirstOrDefaultAsync();
_context.Entry(game).State = EntityState.Modified;
try {
await _context.SaveChangesAsync();
} catch (DbUpdateConcurrencyException) {
if (!GameExists(id)) {
return NotFound();
} else {
throw;
}
}
return NoContent();
if (game == null || game.UserId != user) {
return NotFound();
}
// 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 userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return game;
}
if (!long.TryParse(userIdClaim, out var userId)) {
return Unauthorized();
}
var newGame = new Game {
Deck = (
from shape in Enum.GetValues(typeof(CardShape)).Cast<CardShape>()
from color in Enum.GetValues(typeof(CardColor)).Cast<CardColor>()
from count in Enum.GetValues(typeof(CardCount)).Cast<CardCount>()
from shade in Enum.GetValues(typeof(CardShade)).Cast<CardShade>()
select new Card {
Shape = shape,
Color = color,
Count = count,
Shade = shade
}.ToUshort()).ToArray(),
UserId = userId
};
newGame.ShuffleDeck();
newGame.DealHand();
_context.Games.Add(newGame);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetGame), new { id = newGame.Id }, newGame);
// 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();
}
// DELETE: api/Games/5
[HttpDelete("{id}")]
[Authorize]
public async Task<IActionResult> DeleteGame(long id) {
var game = await _context.Games.FindAsync(id);
if (game == null)
{
return NotFound();
}
var newGame = new Game {
UserId = (long)user,
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()
]
};
_context.Games.Remove(game);
await _context.SaveChangesAsync();
newGame.ShuffleDeck();
newGame.DealHand();
return NoContent();
_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();
}
private bool GameExists(long id) {
return _context.Games.Any(e => e.Id == id);
var game = await _context.Games
.Where(g => g.Id == id && g.UserId == user)
.FirstOrDefaultAsync();
if (game == null) {
return NotFound();
}
[HttpPost]
[Route("[action]/{id}")]
[Authorize]
_context.Games.Remove(game);
await _context.SaveChangesAsync();
return NoContent();
}
[HttpPost]
[Route("[action]/{id}")]
[Authorize]
public async Task<ActionResult<SetCheckResult>> CheckSet(
long id,
[FromBody] ushort[] cardIndices
) {
var game = await _context.Games.FindAsync(id);
if (game == null) {
return NotFound();
}
var res = game.IsSet(cardIndices);
if (res == null) {
return BadRequest();
}
await _context.SaveChangesAsync();
return res;
var user = ParseUserId();
if (user == null) {
return BadRequest();
}
[HttpGet]
[Route("[action]/{id}")]
[Authorize]
public async Task<ActionResult<List<int[]>>> SetsInHand(long id) {
var game = await _context.Games.FindAsync(id);
if (game == null) {
return NotFound();
}
var game = await _context.Games
.Where(g => g.Id == id && g.UserId == user)
.FirstOrDefaultAsync();
var res = game.GetIndicesOfSet();
if (res == null) {
return BadRequest();
}
Console.WriteLine($"Found {res.Count} sets in hand");
return res;
if (game == null) {
return NotFound();
}
var res = game.IsSet(cardIndices);
if (res == null) {
return BadRequest();
}
await _context.SaveChangesAsync();
return res;
}
}
[HttpGet]
[Route("[action]/{id}")]
[Authorize]
public async Task<ActionResult<List<int[]>>> SetsInHand(long id) {
var user = ParseUserId();
if (user == 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.GetIndicesOfSet();
if (res == null) {
return BadRequest();
}
return res;
}
}

View File

@@ -12,8 +12,8 @@ using backend.Models;
namespace backend.Migrations
{
[DbContext(typeof(GameContext))]
[Migration("20250325111947_initial")]
partial class initial
[Migration("20250325145201_InitialDatabase")]
partial class InitialDatabase
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -102,13 +102,11 @@ namespace backend.Migrations
modelBuilder.Entity("backend.Models.Game", b =>
{
b.HasOne("backend.Models.User", "User")
b.HasOne("backend.Models.User", null)
.WithMany("Games")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.User", b =>

View File

@@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace backend.Migrations
{
/// <inheritdoc />
public partial class initial : Migration
public partial class InitialDatabase : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)

View File

@@ -99,13 +99,11 @@ namespace backend.Migrations
modelBuilder.Entity("backend.Models.Game", b =>
{
b.HasOne("backend.Models.User", "User")
b.HasOne("backend.Models.User", null)
.WithMany("Games")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("backend.Models.User", b =>

View File

@@ -18,8 +18,6 @@ public class Game {
public ushort[]? Deck { get; set; }
public ushort[] Found { get; set; } = Array.Empty<ushort>();
public User User { get; set; } = null!;
public void ShuffleDeck() {
if (Deck == null) return;

View File

@@ -11,7 +11,7 @@ interface LoginResponse {
providedIn: 'root'
})
export class AuthService {
private API_URL = 'http://localhost:5224/api/Auth';
private API_URL = 'http://localhost:5224/api/v1/Auth';
private tokenKey = 'auth_token';
private isAuthenticatedSubject = new BehaviorSubject<boolean>(this.hasToken());
public redirectUrl: string | null = null;

View File

@@ -1,13 +1,13 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Card, toCard } from '../../app/models/card';
// todo: Rewrite to use angular http client instead of axios, supports always sending tokens
import axios from 'axios';
axios.defaults.headers.common['Authorization'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6ImRmNThhYTU3LWZkNzItNGIzYS05OTNmLTY4NjAyNGMzYjdlNSIsImV4cCI6MTc0MjgxODEzOCwiaXNzIjoid2Vzc2VsLmdnIiwiYXVkIjoid2Vzc2VsLmdnIn0.hDf8qcxXeSFQhmgnMzBrH3ZJJMplwZ-1RQwNxeZo5ok';
import { lastValueFrom } from 'rxjs';
import { AuthService } from '../auth/auth.service';
@Injectable({ providedIn: 'root' })
export class GameService {
private API_URL = 'http://localhost:5224/api/v1/Games';
public deck: Card[] = [];
public hand: Card[] = [];
public foundSets: Card[][] = [];
@@ -20,8 +20,9 @@ export class GameService {
public finishedAt?: Date;
public selectedCards: Card[] = [];
constructor() {
}
constructor(
private http: HttpClient,
) {}
public initGame(gameId?: string): Promise<string> {
this.deck = [];
@@ -35,58 +36,113 @@ export class GameService {
}
public async initializeExistingGame(gameId: string): Promise<string> {
const req = await axios.get('http://localhost:5224/api/v1/Games/' + gameId);
req.data.deck.forEach((card: number) => {
this.deck.push(toCard(card));
});
req.data.hand.forEach((card: number) => {
try {
const response = await lastValueFrom(this.http.get<any>(`${this.API_URL}/${gameId}`));
response.deck.forEach((card: number) => {
this.deck.push(toCard(card));
});
response.hand.forEach((card: number) => {
this.hand.push(toCard(card));
});
this.startDate = new Date(req.data.startedAt);
this.fails = req.data.fails;
this.hints = req.data.hints;
this.finishedAt = req.data.finishedAt;
this.startDate = new Date(response.startedAt);
this.fails = response.fails;
this.hints = response.hints;
this.finishedAt = response.finishedAt ? new Date(response.finishedAt) : undefined;
this.foundSets = [];
for (let i = 0; i < req.data.found.length; i += 3) {
this.foundSets.push(req.data.found.slice(i, i + 3).map((card: number) => toCard(card)));
for (let i = 0; i < response.found.length; i += 3) {
this.foundSets.push(response.found.slice(i, i + 3).map((card: number) => toCard(card)));
}
this.gameId = req.data.id;
await this.updateSets();
return req.data.id;
this.gameId = response.id;
this.updateSets();
return response.id.toString();
} catch (error) {
console.error('Error initializing existing game', error);
throw error;
}
}
private async initializeDeck(): Promise<string> {
const res = await axios.post('http://localhost:5224/api/v1/Games', {});
res.data.deck.forEach((card: number) => {
try {
const response = await lastValueFrom(this.http.post<any>(this.API_URL, {}));
response.deck.forEach((card: number) => {
this.deck.push(toCard(card));
});
res.data.hand.forEach((card: number) => {
});
response.hand.forEach((card: number) => {
this.hand.push(toCard(card));
});
this.startDate = new Date(res.data.startedAt);
this.fails = res.data.fails;
this.hints = res.data.hints;
this.gameId = res.data.id;
this.finishedAt = res.data.finishedAt;
this.startDate = new Date(response.startedAt);
this.fails = response.fails;
this.hints = response.hints;
this.gameId = response.id;
this.finishedAt = response.finishedAt ? new Date(response.finishedAt) : undefined;
this.foundSets = [];
for (let i = 0; i < res.data.newState.found.length; i += 3) {
this.foundSets.push(res.data.newState.found.slice(i, i + 3).map((card: number) => toCard(card)));
for (let i = 0; i < response.found.length; i += 3) {
this.foundSets.push(response.found.slice(i, i + 3).map((card: number) => toCard(card)));
}
this.updateSets();
return response.id.toString();
} catch (error) {
console.error('Error initializing deck', error);
throw error;
}
}
public async updateSets(): Promise<Card[][]> {
try {
const response = await lastValueFrom(
this.http.get<number[][]>(`${this.API_URL}/SetsInHand/${this.gameId}`)
);
const cards = response.map((set: number[]) =>
set.map((card: number) => this.hand[card])
);
this.possibleSets = cards;
return cards;
} catch (error) {
console.error('Error updating sets', error);
throw error;
}
}
public async checkSet(cards: number[]): Promise<boolean> {
try {
const response = await lastValueFrom(
this.http.post<any>(`${this.API_URL}/CheckSet/${this.gameId}`, cards)
);
this.hand = response.newState.hand.map((card: number) => toCard(card));
this.deck = response.newState.deck.map((card: number) => toCard(card));
this.fails = response.newState.fails;
this.hints = response.newState.hints;
this.finishedAt = response.newState.finishedAt ? new Date(response.newState.finishedAt) : undefined;
this.foundSets = [];
for (let i = 0; i < response.newState.found.length; i += 3) {
this.foundSets.push(response.newState.found.slice(i, i + 3).map((card: number) => toCard(card)));
}
await this.updateSets();
return res.data.id;
return response.isSet;
} catch (error) {
console.error('Error checking set', error);
throw error;
}
}
public selectCard(card: Card): void {
const cardIndex = this.hand.indexOf(card);
if (cardIndex === -1) return; // Card not found on the board
if (cardIndex === -1) return; // Card not found in hand
if (this.selectedCards.includes(card)) {
this.selectedCards = this.selectedCards.filter(c => c !== card);
@@ -95,58 +151,29 @@ export class GameService {
}
if (this.selectedCards.length === 3) {
const [card1, card2, card3] = this.selectedCards as [Card, Card, Card]; // ✅ Explicitly cast to a tuple
if (this.isSet([this.hand.indexOf(card1), this.hand.indexOf(card2), this.hand.indexOf(card3)])) {
this.replaceSet();
}
const indices = this.selectedCards.map(c => this.hand.indexOf(c));
this.checkSet(indices).then(isSet => {
this.selectedCards = [];
});
}
}
public async updateSets(): Promise<Card[][]> {
const req = await axios.get(`http://localhost:5224/api/v1/Games/SetsInHand/${this.gameId}`);
const cards = req.data
.map((set: number[]) =>
set.map((card: number) => this.hand[card])
);
this.possibleSets = cards;
return cards;
public async deleteGame(): Promise<void> {
try {
await lastValueFrom(this.http.delete<void>(`${this.API_URL}/${this.gameId}`));
} catch (error) {
console.error('Error deleting game', error);
throw error;
}
}
private isSet(cards: number[]): boolean {
this.selectedCards = [];
axios.post(`http://localhost:5224/api/v1/Games/CheckSet/${this.gameId}`, cards).then((response) => {
this.hand = response.data.newState.hand.map((card: number) => toCard(card));
this.deck = response.data.newState.deck.map((card: number) => toCard(card));
this.fails = response.data.newState.fails;
this.hints = response.data.newState.hints;
this.finishedAt = response.data.newState.finishedAt;
this.foundSets = [];
for (let i = 0; i < response.data.newState.found.length; i += 3) {
this.foundSets.push(response.data.newState.found.slice(i, i + 3).map((card: number) => toCard(card)));
}
this.updateSets();
return response.data.isSet;
});
return false;
public async showHint(): Promise<void> {
try {
await lastValueFrom(this.http.post<void>(`${this.API_URL}/Hint/${this.gameId}`, {}));
this.hints++;
} catch (error) {
console.error('Error showing hint', error);
throw error;
}
}
private replaceSet(): void {
this.hand.find((c, i) => {
if (this.selectedCards.includes(c)) {
this.hand[i] = this.deck.splice(0, 1)[0];
}
});
this.selectedCards = [];
}
public async deleteGame() {
await axios.delete('http://localhost:5224/api/v1/Games/' + this.gameId);
}
}
}

View File

@@ -1,43 +1,40 @@
import { Injectable } from '@angular/core';
import axios, { AxiosError } from 'axios';
// axios.defaults.headers.common['Authorization'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6ImRmNThhYTU3LWZkNzItNGIzYS05OTNmLTY4NjAyNGMzYjdlNSIsImV4cCI6MTc0MjgxODEzOCwiaXNzIjoid2Vzc2VsLmdnIiwiYXVkIjoid2Vzc2VsLmdnIn0.hDf8qcxXeSFQhmgnMzBrH3ZJJMplwZ-1RQwNxeZo5ok';
axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
import { HttpClient } from '@angular/common/http';
import { lastValueFrom } from 'rxjs';
import { AuthService } from '../auth/auth.service';
@Injectable({
providedIn: 'root'
})
export class UserDataService {
private GAMES_API_URL = 'http://localhost:5224/api/v1/Games';
constructor() {
// this.login({ username: 'admin', password: 'password' });
constructor(
private http: HttpClient,
private authService: AuthService
) {}
public async login(credentials: { username: string, password: string }): Promise<void> {
try {
await lastValueFrom(this.authService.login(credentials.username, credentials.password));
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
public async login(credentials: any) {
const response = await axios.post('http://localhost:5224/api/Auth/login', credentials);
localStorage.setItem('token', response.data.token);
public async getGames(): Promise<any[]> {
try {
const data = await lastValueFrom(this.http.get<any[]>(this.GAMES_API_URL));
const sortedGames = data
.sort((a: any, b: any) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime())
.reverse();
return sortedGames;
} catch (error) {
console.error('Error getting games:', error);
throw error;
}
}
public async getGames(): Promise<any> {
const req = await axios.get('http://localhost:5224/api/v1/Games', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6ImRmNThhYTU3LWZkNzItNGIzYS05OTNmLTY4NjAyNGMzYjdlNSIsImV4cCI6MTc0MjgxODEzOCwiaXNzIjoid2Vzc2VsLmdnIiwiYXVkIjoid2Vzc2VsLmdnIn0.hDf8qcxXeSFQhmgnMzBrH3ZJJMplwZ-1RQwNxeZo5ok'
}
});
const sortedGames = req.data
.sort((a: any, b: any) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime())
.reverse();
return sortedGames;
}
}
}