diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4e951fe..0132e36 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: - dotnet-version: 5.0.201 + dotnet-version: '6.0.x' - name: Restore dependencies run: dotnet restore - name: Build diff --git a/Client/Client - Backup.csproj b/Client/Client - Backup.csproj deleted file mode 100644 index 9405ab1..0000000 --- a/Client/Client - Backup.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - Exe - net5.0 - - - - - - - - - - - - diff --git a/Client/Client.Account/Client.Account.csproj b/Client/Client.Account/Client.Account.csproj new file mode 100644 index 0000000..d0005cc --- /dev/null +++ b/Client/Client.Account/Client.Account.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/Client/Client.Account/Enums/MenuTypes.cs b/Client/Client.Account/Enums/MenuTypes.cs new file mode 100644 index 0000000..614a7ff --- /dev/null +++ b/Client/Client.Account/Enums/MenuTypes.cs @@ -0,0 +1,9 @@ +namespace Client.Account.Enums; + +internal enum MenuTypes +{ + Unknown = 0, + SignUp = 1, + Login = 2, + Back = 3, +} \ No newline at end of file diff --git a/Client/Client.Account/Extensions/EnumExtensions.cs b/Client/Client.Account/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..03c3339 --- /dev/null +++ b/Client/Client.Account/Extensions/EnumExtensions.cs @@ -0,0 +1,39 @@ +using Client.Account.Enums; + +namespace Client.Account.Extensions; + +internal static class EnumExtensions +{ + internal static string GetDisplayName(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.Back => "Back", + MenuTypes.Login => "Login", + MenuTypes.SignUp => "Sign Up", + _ => "Unknown", + }; + } + + internal static MenuTypes TryGetMenuType(this string? stringInput) + { + return stringInput switch + { + "3" => MenuTypes.Back, + "2" => MenuTypes.Login, + "1" => MenuTypes.SignUp, + _ => MenuTypes.Unknown, + }; + } + + internal static int GetValue(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.Back => 3, + MenuTypes.Login => 2, + MenuTypes.SignUp => 1, + _ => 0, + }; + } +} \ No newline at end of file diff --git a/Client/Client.Account/Extensions/ServiceCollectionExtensions.cs b/Client/Client.Account/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..476abe6 --- /dev/null +++ b/Client/Client.Account/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using Client.Account.Menus; +using Client.Account.Services; +using Client.Account.Services.Interfaces; +using RockPaperScissors.Common.Client; + +namespace Client.Account.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IAccountService CreateAccountService(this IClient client) + { + return new AccountService(client); + } + + public static IAccountMenu CreateAccountMenu(this IAccountService accountService) + { + return new AccountMenu(accountService); + } +} \ No newline at end of file diff --git a/Client/Client.Account/Menus/AccountMenu.cs b/Client/Client.Account/Menus/AccountMenu.cs new file mode 100644 index 0000000..b467d1f --- /dev/null +++ b/Client/Client.Account/Menus/AccountMenu.cs @@ -0,0 +1,160 @@ +using Client.Account.Enums; +using Client.Account.Extensions; +using Client.Account.Services; +using Client.Account.Services.Interfaces; +using RockPaperScissors.Common.Enums; +using RockPaperScissors.Common.Extensions; + +namespace Client.Account.Menus; + +internal sealed class AccountMenu : IAccountMenu +{ + private readonly IAccountService _accountService; + + public AccountMenu(IAccountService accountService) + { + _accountService = accountService ?? throw new ArgumentNullException(nameof(accountService)); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + "Account Menu:".Print(ConsoleColor.DarkYellow); + $"{MenuTypes.SignUp.GetValue()}.\t{MenuTypes.SignUp.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + $"{MenuTypes.Login.GetValue()}.\t{MenuTypes.Login.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + $"{MenuTypes.Back.GetValue()}.\t{MenuTypes.Back.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + + "\nPlease select an item from the list".Print(ConsoleColor.Green); + + Console.Write("Select -> "); + + var menuType = Console.ReadLine().TryGetMenuType(); + + if (menuType is MenuTypes.Unknown) + { + "Invalid input. Try again.".Print(ConsoleColor.Red); + + continue; + } + + switch (menuType) + { + case MenuTypes.SignUp: + var isRegistered = await RegisterAsync(cancellationToken); + + if (!isRegistered) + { + "\nPress any key to back to the start up menu list!".Print(ConsoleColor.Cyan); + Console.ReadKey(); + Console.Clear(); + + continue; + } + + "Trying logging in...".Print(ConsoleColor.White); + Console.Clear(); + + return; + + case MenuTypes.Login: + var isSuccess = await LoginAsync(cancellationToken); + + if (isSuccess) + { + "\nPress any key to back to the start up menu list!".Print(ConsoleColor.Cyan); + Console.ReadKey(); + Console.Clear(); + + return; + } + + continue; + + case MenuTypes.Back: + Console.Clear(); + + return; + + default: + "Invalid input. Try again.".Print(ConsoleColor.Red); + continue; + } + } + } + + public async Task LogoutAsync(CancellationToken cancellationToken) + { + if (!_accountService.IsAuthorized()) + { + return true; + } + + var user = _accountService.GetUser(); + + var response = await _accountService.LogoutAsync(user.SessionId, cancellationToken); + + return response.Match( + _ => + { + "Successfully signed out".Print(ConsoleColor.DarkGreen); + + return true; + }, + exception => + { + exception.Message.Print(ConsoleColor.Red); + + return false; + }); + } + + private async Task RegisterAsync(CancellationToken cancellationToken) + { + ("We are glad to welcome you in the registration form!\n" + + "Please enter the required details\n" + + "to register an account on the platform") + .Print(ConsoleColor.Magenta); + + var login = "Login: ".BuildString(StringDestination.Login); + var password = "Password: ".BuildString(StringDestination.Password); + + var response = await _accountService.SignUpAsync(login, password, cancellationToken); + + return response.Match( + _ => + { + "Successfully registered!".Print(ConsoleColor.Green); + + return true; + }, + exception => + { + exception.Message.Print(ConsoleColor.Red); + + return false; + }); + } + + private async Task LoginAsync(CancellationToken cancellationToken) + { + var login = "Login: ".BuildString(StringDestination.Login); + var password = "Password: ".BuildString(StringDestination.Password, isNeedConfirmation: true); + + var response = await _accountService.LoginAsync(login, password, cancellationToken); + + return response.Match( + _ => + { + "Successfully signed in".Print(ConsoleColor.DarkGreen); + + return true; + }, + exception => + { + exception.Message.Print(ConsoleColor.Red); + + return false; + }); + } +} \ No newline at end of file diff --git a/Client/Client.Account/Menus/IAccountMenu.cs b/Client/Client.Account/Menus/IAccountMenu.cs new file mode 100644 index 0000000..49d6cab --- /dev/null +++ b/Client/Client.Account/Menus/IAccountMenu.cs @@ -0,0 +1,8 @@ +namespace Client.Account.Menus; + +public interface IAccountMenu +{ + Task StartAsync(CancellationToken cancellationToken); + + Task LogoutAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Client/Client.Account/Services/AccountService.cs b/Client/Client.Account/Services/AccountService.cs new file mode 100644 index 0000000..db08747 --- /dev/null +++ b/Client/Client.Account/Services/AccountService.cs @@ -0,0 +1,105 @@ +using Client.Account.Services.Interfaces; +using OneOf; +using RockPaperScissors.Common; +using RockPaperScissors.Common.Client; +using RockPaperScissors.Common.Extensions; +using RockPaperScissors.Common.Requests; +using RockPaperScissors.Common.Responses; + +namespace Client.Account.Services; + +internal sealed class AccountService : IAccountService +{ + private readonly IClient _client; + private IUser _user = null!; + + public AccountService(IClient client) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + } + + public Task> SignUpAsync( + string login, string password, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(login)) + { + return Task.FromResult(OneOf.FromT1(new CustomException("Login must not be 'null' or '\"\"'"))); + } + + if (string.IsNullOrEmpty(password)) + { + return Task.FromResult(OneOf.FromT1(new CustomException("Password must not be 'null' or '\"\"'"))); + } + + var request = new RegisterRequest + { + Login = login, + Password = password + }; + + var response = + _client.PostAsync("api/Account/register", request, cancellationToken); + + return response; + } + + public async Task> LoginAsync( + string login, string password, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(login)) + { + return new CustomException("Login must not be 'null' or '\"\"'"); + } + + if (string.IsNullOrEmpty(password)) + { + return new CustomException("Password must not be 'null' or '\"\"'"); + } + + var request = new LoginRequest + { + Login = login, + Password = password + }; + + var response = + await _client.PostAsync("api/Account/login", request, cancellationToken); + + if (!response.IsT0) + { + return response; + } + + var loginResponse = response.AsT0; + _user = new User(loginResponse.Token, loginResponse.Login); + + return response; + } + + public Task> LogoutAsync( + string? token, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(token)) + { + return Task.FromResult(OneOf.FromT1(new CustomException("Token must not be 'null' or '\"\"'"))); + } + + var queryUrl = new QueryBuilder(1) + { + { "sessionId", token } + }; + var url = $"api/Account/logout{queryUrl}"; + + var response = + _client.GetAsync(url, ("Authorization", token), cancellationToken); + + return response; + } + + public IUser GetUser() => _user; + + public bool IsAuthorized() + { + return _user is not null && _user.IsAuthorized; + } +} \ No newline at end of file diff --git a/Client/Client.Account/Services/Interfaces/IAccountService.cs b/Client/Client.Account/Services/Interfaces/IAccountService.cs new file mode 100644 index 0000000..0354256 --- /dev/null +++ b/Client/Client.Account/Services/Interfaces/IAccountService.cs @@ -0,0 +1,25 @@ +using OneOf; +using RockPaperScissors.Common; +using RockPaperScissors.Common.Responses; + +namespace Client.Account.Services.Interfaces; + +public interface IAccountService +{ + Task> SignUpAsync(string login, + string password, + CancellationToken cancellationToken = default); + + Task> LoginAsync( + string login, + string password, + CancellationToken cancellationToken = default); + + Task> LogoutAsync( + string? token, + CancellationToken cancellationToken = default); + + IUser GetUser(); + + bool IsAuthorized(); +} \ No newline at end of file diff --git a/Client/Client.Account/Services/Interfaces/IUser.cs b/Client/Client.Account/Services/Interfaces/IUser.cs new file mode 100644 index 0000000..998c032 --- /dev/null +++ b/Client/Client.Account/Services/Interfaces/IUser.cs @@ -0,0 +1,12 @@ +namespace Client.Account.Services.Interfaces; + +public interface IUser +{ + public string? SessionId { get; } + + public string? Login { get; } + + bool IsAuthorized => !string.IsNullOrEmpty(SessionId) && !string.IsNullOrEmpty(Login); + + string GetBearerToken(); +} \ No newline at end of file diff --git a/Client/Client.Account/Services/User.cs b/Client/Client.Account/Services/User.cs new file mode 100644 index 0000000..39f5692 --- /dev/null +++ b/Client/Client.Account/Services/User.cs @@ -0,0 +1,27 @@ +using Client.Account.Services.Interfaces; + +namespace Client.Account.Services; + +internal sealed class User : IUser +{ + public static readonly User Default = new(string.Empty, string.Empty); + + internal User(string sessionId, string login) + { + SessionId = sessionId; + Login = login; + } + + public string SessionId { get; } + + public string Login { get; } + + + public bool IsAuthorized => !string.IsNullOrEmpty(SessionId) && !string.IsNullOrEmpty(Login); + + + public string GetBearerToken() + { + return $"Bearer {SessionId}"; + } +} \ No newline at end of file diff --git a/Client/Client.Host/Client.Host.csproj b/Client/Client.Host/Client.Host.csproj new file mode 100644 index 0000000..5bfac12 --- /dev/null +++ b/Client/Client.Host/Client.Host.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6 + enable + + + + + + + + + + + + + diff --git a/Client/ClientAppEmulator.cs b/Client/Client.Host/ClientAppEmulator.cs similarity index 91% rename from Client/ClientAppEmulator.cs rename to Client/Client.Host/ClientAppEmulator.cs index 1efd022..6adbb76 100644 --- a/Client/ClientAppEmulator.cs +++ b/Client/Client.Host/ClientAppEmulator.cs @@ -1,18 +1,18 @@ -using System; +/*using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using Client.Models; -using Client.Models.Interfaces; -using Client.Services; -using Client.Services.RequestProcessor; -using Client.Services.RequestProcessor.RequestModels.Impl; +using Client.Host.Models; +using Client.Host.Models.Interfaces; +using Client.Host.Services; +using Client.Host.Services.RequestProcessor; +using Client.Host.Services.RequestProcessor.RequestModels.Impl; using Newtonsoft.Json; using NLog; -namespace Client +namespace Client.Host { public class ClientAppEmulator { @@ -135,63 +135,7 @@ private async Task StartMenu(CancellationToken token) } } - private async Task PlayerMenu() - { - while (true) - { - ColorTextWriterService.PrintLineMessageWithSpecialColor($"***\nHello, {_playerAccount.Login}\n" + - "Please choose option", ConsoleColor.Cyan); - ColorTextWriterService.PrintLineMessageWithSpecialColor("1.\tPlay with bot\n" + - "2\tCreate room\n" + - "3\tJoin Private room\n" + - "4\tJoin Public room\n" + - "5\tShow Statistics\n" + - "6\tLog out", ConsoleColor.Yellow); - - ColorTextWriterService.PrintLineMessageWithSpecialColor("\nPlease select an item from the list", ConsoleColor.Green); - Console.Write("Select -> "); - var passed = int.TryParse(Console.ReadLine(), out int playersMenuInput); - if (!passed) - { - logger.Trace($"Not passed argument to player Menu"); - ColorTextWriterService.PrintLineMessageWithSpecialColor("Unsupported input", ConsoleColor.Red); - continue; - } - switch (playersMenuInput) - { - case 1: - Console.Clear(); - await JoinRoomWithBot(); - break; - case 2: - Console.Clear(); - await CreationRoom(); - break; - case 3: - Console.Clear(); - await JoinPrivateRoom(); - break; - case 4: - Console.Clear(); - await JoinPublicRoom(); - break; - case 5: - Console.Clear(); - var statistics = await PersonalStatistics(_sessionId); - Console.WriteLine(statistics+"\n\nPress any key to go back."); - Console.ReadKey(); - break; - case 6: - Console.Clear(); - await Logout(); - return; - default: - ColorTextWriterService.PrintLineMessageWithSpecialColor("Unsupported input", ConsoleColor.Red); - continue; - } - } - } private async Task JoinRoomWithBot() { logger.Trace("JoinRoomWithBot method"); @@ -280,7 +224,7 @@ private async Task JoinPrivateRoom() logger.Info("Error occured. Probably there is either no room or it is already full"); } } - /**/ + /*#1# private async Task StartRoomMenu() { @@ -312,7 +256,7 @@ private async Task StartRoomMenu() Console.ReadKey(); } } - /**/ + /*#1# private async Task MakeYourMove() { var move = 0; @@ -710,6 +654,4 @@ private static void PrintStatistics(IEnumerable statisticsEnumera } } } -} - - \ No newline at end of file +}*/ \ No newline at end of file diff --git a/Client/Client.Host/Menus/IMainMenu.cs b/Client/Client.Host/Menus/IMainMenu.cs new file mode 100644 index 0000000..40e613a --- /dev/null +++ b/Client/Client.Host/Menus/IMainMenu.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Client.Host.Menus; + +public interface IMainMenu +{ + Task PlayerMenu(); +} \ No newline at end of file diff --git a/Client/Client.Host/Menus/MainMenu.cs b/Client/Client.Host/Menus/MainMenu.cs new file mode 100644 index 0000000..99aae7f --- /dev/null +++ b/Client/Client.Host/Menus/MainMenu.cs @@ -0,0 +1,87 @@ +/*using System; +using System.Threading.Tasks; +using Client.Host.Models; +using Client.Host.Services; +using Client.Host.Services.RequestProcessor; + +namespace Client.Host.Menus; + +public class MainMenu : IMainMenu +{ + private readonly TokenModel _playerAccount; + private readonly IRoomService _roomService; + private readonly IStatisticsService _statisticsService; + + public MainMenu(TokenModel account, + IRequestPerformer requestPerformer, + IStatisticsService statisticsService) + { + _playerAccount = account; + _statisticsService = statisticsService; + _roomService = new RoomService(_playerAccount, requestPerformer); + } + + public async Task PlayerMenu() + { + while (true) + { + TextWrite.Print( + $"***\nHello, {_playerAccount.Login}\n" + + "Please choose option", ConsoleColor.Cyan); + TextWrite.Print( + "1.\tPlay with bot\n" + + "2\tCreate room\n" + + "3\tJoin room\n" + + "4\tSearch open room\n" + + "5\tShow Statistics\n" + + "6\tLog out", ConsoleColor.Yellow); + + TextWrite.Print("\nPlease select an item from the list", ConsoleColor.Green); + + Console.Write("Select -> "); + var passed = int.TryParse(Console.ReadLine(), out var playersMenuInput); + if (!passed) + { + TextWrite.Print("Unsupported input", ConsoleColor.Red); + continue; + } + switch (playersMenuInput) + { + case 1: + Console.Clear(); + //await JoinRoomWithBot(); + var room = await _roomService.CreateRoom(true, true); + if (room is null) + return; + //todo: redirect somewhere + break; + case 2: + Console.Clear(); + //await CreationRoom(); + break; + case 3: + Console.Clear(); + //await JoinPrivateRoom(); + break; + case 4: + Console.Clear(); + //await JoinPublicRoom(); + break; + case 5: + Console.Clear(); + var statistics = await _statisticsService + .GetPersonalStatistics(_playerAccount.BearerToken); + Console.WriteLine(statistics+"\n\nPress any key to go back."); + Console.ReadKey(); + break; + case 6: + Console.Clear(); + //await Logout(); + return; + default: + TextWrite.Print("Unsupported input", ConsoleColor.Red); + continue; + } + } + } +}*/ \ No newline at end of file diff --git a/Client/Client.Host/Program.cs b/Client/Client.Host/Program.cs new file mode 100644 index 0000000..0ddc2a5 --- /dev/null +++ b/Client/Client.Host/Program.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Client.Account.Extensions; +using Client.StartMenu.Extensions; +using Client.Statistics.Extensions; + +namespace Client.Host; + +internal static class Program +{ + private static async Task Main() + { + var cancellationTokenSource = new CancellationTokenSource(); + + try + { + var baseAddress = "http://localhost:5000"; + var client = RockPaperScissors.Common.Client.Client.Create(baseAddress); + var accountService = client.CreateAccountService(); + var statisticsService = client.CreateStatisticsService(); + var healthCheckService = client.CreateHealthCheckService(cancellationTokenSource); + + var accountMenu = accountService.CreateAccountMenu(); + var statisticsMenu = statisticsService.CreateStatisticsMenu(accountService); + var startMenu = statisticsMenu.CreateStartMenu(accountMenu, healthCheckService); + + await startMenu.PrintAsync(cancellationTokenSource.Token); + + return 0; + } + catch (TaskCanceledException) + { + return -1; + } + catch (Exception exception) + { + Console.WriteLine(exception.Message); + Console.WriteLine("Unknown error occured. Closing."); + + return -1; + } + } +} \ No newline at end of file diff --git a/Client/Client.Host/Services/RoomService.cs b/Client/Client.Host/Services/RoomService.cs new file mode 100644 index 0000000..86e5e6c --- /dev/null +++ b/Client/Client.Host/Services/RoomService.cs @@ -0,0 +1,59 @@ +// using Client.Host.Extensions; +// using Client.Host.Models; +// using Client.Host.Services.RequestProcessor; +// using Client.Host.Services.RequestProcessor.RequestModels; +// using Client.Host.Services.RequestProcessor.RequestModels.Impl; +// using Newtonsoft.Json; +// +// namespace Client.Host.Services; +// +// public class RoomService : IRoomService +// { +// private readonly TokenModel _tokenModel; +// private readonly IRequestPerformer _requestPerformer; +// +// public RoomService(TokenModel tokenModel, +// IRequestPerformer requestPerformer) +// { +// _tokenModel = tokenModel; +// _requestPerformer = requestPerformer; +// } +// public async Task CreateRoom(bool isPrivate, bool isTraining) +// { +// Console.WriteLine("Trying to create a room."); +// +// var options = new RequestOptions +// { +// Address = $"room/create?isPrivate={isPrivate}", +// IsValid = true, +// Headers = new Dictionary +// { +// { +// "Authorization", _tokenModel.BearerToken +// }, +// { +// "X-Training", isTraining.ToString() +// } +// }, +// Method = RequestMethod.Post, +// Body = string.Empty, +// Name = "Create Room" +// }; +// var reachedResponse = await _requestPerformer +// .PerformRequestAsync(options); +// +// if (reachedResponse.TryParseJson(out var errorModel)) +// { +// TextWrite.Print(errorModel.Message, ConsoleColor.Red); +// return null; +// } +// +// var room = JsonConvert.DeserializeObject(reachedResponse.Content); +// return room; +// } +// } +// +// public interface IRoomService +// { +// Task CreateRoom(bool isPrivate, bool isTraining); +// } \ No newline at end of file diff --git a/Client/Client.Host/Services/StatisticsService.cs b/Client/Client.Host/Services/StatisticsService.cs new file mode 100644 index 0000000..f8ce6e0 --- /dev/null +++ b/Client/Client.Host/Services/StatisticsService.cs @@ -0,0 +1,74 @@ +// using System; +// using System.Collections.Generic; +// using System.Threading.Tasks; +// using Client.Host.Models; +// using Client.Host.Models.Interfaces; +// using Client.Host.Services.RequestProcessor; +// using Client.Host.Services.RequestProcessor.RequestModels; +// using Client.Host.Services.RequestProcessor.RequestModels.Impl; +// using Mapster; +// using Newtonsoft.Json; +// +// namespace Client.Host.Services; +// +// public class StatisticsService: IStatisticsService +// { +// private readonly IRequestPerformer _requestPerformer; +// +// public StatisticsService(IRequestPerformer requestPerformer) +// { +// _requestPerformer = requestPerformer; +// } +// +// public async Task GetAllStatistics() +// { +// var options = new RequestOptions +// { +// ContentType = "none", +// Address = "stats/all", +// IsValid = true, +// Method = RequestMethod.Get, +// Name = "OverallStats" +// }; +// +// var response = await _requestPerformer.PerformRequestAsync(options); +// +// var toConvert = JsonConvert.DeserializeObject(response.Content); +// return toConvert?.Adapt(); +// } +// +// public async Task GetPersonalStatistics(string token) +// { +// var options = new RequestOptions +// { +// Headers = new Dictionary{{"Authorization",token}}, +// ContentType = "none", +// Address = "stats/personal", +// IsValid = true, +// Method = RequestMethod.Get, +// Name = "PersonalStats" +// }; +// +// var response = await _requestPerformer.PerformRequestAsync(options); +// +// return response.Content != null ? JsonConvert.DeserializeObject(response.Content) : null; +// } +// +// public Task PrintStatistics(IOverallStatistics[] statistics) +// { +// for(var i = 0; i < statistics.Length; i++) +// { +// TextWrite.Print($"{i+1}. User: {statistics[i].Account.Login}\n" + +// $"Score: {statistics[i].Score}", ConsoleColor.White); +// } +// Console.Write('\n'); +// return Task.CompletedTask; +// } +// } +// +// public interface IStatisticsService +// { +// Task GetAllStatistics(); +// Task GetPersonalStatistics(string token); +// Task PrintStatistics(IOverallStatistics[] statistics); +// } \ No newline at end of file diff --git a/Client/Client.MainMenu/Client.MainMenu.csproj b/Client/Client.MainMenu/Client.MainMenu.csproj new file mode 100644 index 0000000..eb2460e --- /dev/null +++ b/Client/Client.MainMenu/Client.MainMenu.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Client/Client.MainMenu/Enums/MenuTypes.cs b/Client/Client.MainMenu/Enums/MenuTypes.cs new file mode 100644 index 0000000..adf197d --- /dev/null +++ b/Client/Client.MainMenu/Enums/MenuTypes.cs @@ -0,0 +1,10 @@ +namespace Client.MainMenu.Enums; + +internal enum MenuTypes +{ + Unknown = 0, + PersonalScoreboard = 1, + CreateRoom = 2, + Logout = 3, + Exit = 4, +} \ No newline at end of file diff --git a/Client/Client.MainMenu/Extensions/EnumExtensions.cs b/Client/Client.MainMenu/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..0fafffd --- /dev/null +++ b/Client/Client.MainMenu/Extensions/EnumExtensions.cs @@ -0,0 +1,42 @@ +using Client.MainMenu.Enums; + +namespace Client.MainMenu.Extensions; + +internal static class EnumExtensions +{ + internal static string GetDisplayName(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.PersonalScoreboard => "Personal Statistics", + MenuTypes.CreateRoom => "Create room", + MenuTypes.Logout => "Logout", + MenuTypes.Exit => "Exit", + _ => "Unknown", + }; + } + + internal static MenuTypes TryGetMenuType(this string? stringInput) + { + return stringInput switch + { + "4" => MenuTypes.Exit, + "3" => MenuTypes.Logout, + "2" => MenuTypes.CreateRoom, + "1" => MenuTypes.PersonalScoreboard, + _ => MenuTypes.Unknown, + }; + } + + internal static int GetValue(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.Exit => 4, + MenuTypes.Logout => 3, + MenuTypes.CreateRoom => 2, + MenuTypes.PersonalScoreboard => 1, + _ => 0, + }; + } +} \ No newline at end of file diff --git a/Client/Client.MainMenu/Extensions/ServiceCollectionExtensions.cs b/Client/Client.MainMenu/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..6001356 --- /dev/null +++ b/Client/Client.MainMenu/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +namespace Client.MainMenu.Extensions; + +// public static class ServiceCollectionExtensions +// { +// public static IAccountService CreateAccountService(this IClient client) +// { +// return new AccountService(client); +// } +// +// public static IAccountMenu CreateAccountMenu(this IAccountService accountService) +// { +// return new AccountMenu(accountService); +// } +// } \ No newline at end of file diff --git a/Client/Client.MainMenu/Services/RoomService.cs b/Client/Client.MainMenu/Services/RoomService.cs new file mode 100644 index 0000000..d0b0218 --- /dev/null +++ b/Client/Client.MainMenu/Services/RoomService.cs @@ -0,0 +1,11 @@ +namespace Client.MainMenu.Services; + +public interface IRoomService +{ + //Todo : implement +} + +internal sealed class RoomService : IRoomService +{ + //Todo : implement +} \ No newline at end of file diff --git a/Client/Client.MainMenu/Services/RoundService.cs b/Client/Client.MainMenu/Services/RoundService.cs new file mode 100644 index 0000000..2dbfc6a --- /dev/null +++ b/Client/Client.MainMenu/Services/RoundService.cs @@ -0,0 +1,11 @@ +namespace Client.MainMenu.Services; + +public interface IRoundService +{ + //Todo : implement +} + +internal sealed class RoundService : IRoundService +{ + //Todo : implement +} \ No newline at end of file diff --git a/Client/Client.StartMenu/Client.StartMenu.csproj b/Client/Client.StartMenu/Client.StartMenu.csproj new file mode 100644 index 0000000..4d8587f --- /dev/null +++ b/Client/Client.StartMenu/Client.StartMenu.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/Client/Client.StartMenu/Enums/MenuTypes.cs b/Client/Client.StartMenu/Enums/MenuTypes.cs new file mode 100644 index 0000000..c9524ea --- /dev/null +++ b/Client/Client.StartMenu/Enums/MenuTypes.cs @@ -0,0 +1,9 @@ +namespace Client.StartMenu.Enums; + +internal enum MenuTypes +{ + Unknown = 0, + Account = 1, + Leaderboard = 2, + Exit = 3, +} \ No newline at end of file diff --git a/Client/Client.StartMenu/Extensions/EnumExtensions.cs b/Client/Client.StartMenu/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..3f1898a --- /dev/null +++ b/Client/Client.StartMenu/Extensions/EnumExtensions.cs @@ -0,0 +1,39 @@ +using Client.StartMenu.Enums; + +namespace Client.StartMenu.Extensions; + +internal static class EnumExtensions +{ + internal static string GetDisplayName(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.Account => "Account", + MenuTypes.Leaderboard => "Leaderboard", + MenuTypes.Exit => "Exit", + _ => "Unknown", + }; + } + + internal static MenuTypes TryGetMenuType(this string? stringInput) + { + return stringInput switch + { + "3" => MenuTypes.Exit, + "2" => MenuTypes.Leaderboard, + "1" => MenuTypes.Account, + _ => MenuTypes.Unknown, + }; + } + + internal static int GetValue(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.Exit => 3, + MenuTypes.Leaderboard => 2, + MenuTypes.Account => 1, + _ => 0, + }; + } +} \ No newline at end of file diff --git a/Client/Client.StartMenu/Extensions/ServiceCollectionExtensions.cs b/Client/Client.StartMenu/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..c8f39c7 --- /dev/null +++ b/Client/Client.StartMenu/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Client.Account.Menus; +using Client.StartMenu.Menus; +using Client.StartMenu.Services; +using Client.Statistics.Menus; +using RockPaperScissors.Common.Client; + +namespace Client.StartMenu.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IStartMenu CreateStartMenu( + this IStatisticsMenu statisticsMenu, + IAccountMenu accountMenu, + IHealthCheckService healthCheckService) + { + ArgumentNullException.ThrowIfNull(accountMenu); + ArgumentNullException.ThrowIfNull(statisticsMenu); + + return new Menus.StartMenu(accountMenu, statisticsMenu, healthCheckService); + } + + public static IHealthCheckService CreateHealthCheckService( + this IClient client, + CancellationTokenSource cancellationTokenSource) + { + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(cancellationTokenSource); + + return new HealthCheckService(client, cancellationTokenSource); + } +} \ No newline at end of file diff --git a/Client/Client.StartMenu/Menus/IStartMenu.cs b/Client/Client.StartMenu/Menus/IStartMenu.cs new file mode 100644 index 0000000..81967ba --- /dev/null +++ b/Client/Client.StartMenu/Menus/IStartMenu.cs @@ -0,0 +1,6 @@ +namespace Client.StartMenu.Menus; + +public interface IStartMenu +{ + Task PrintAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Client/Client.StartMenu/Menus/StartMenu.cs b/Client/Client.StartMenu/Menus/StartMenu.cs new file mode 100644 index 0000000..7053a10 --- /dev/null +++ b/Client/Client.StartMenu/Menus/StartMenu.cs @@ -0,0 +1,101 @@ +using Client.Account.Menus; +using Client.StartMenu.Enums; +using Client.StartMenu.Extensions; +using Client.StartMenu.Services; +using Client.Statistics.Menus; +using RockPaperScissors.Common.Extensions; + +namespace Client.StartMenu.Menus; + +internal sealed class StartMenu: IStartMenu +{ + private readonly IAccountMenu _accountMenu; + private readonly IStatisticsMenu _statisticsMenu; + private readonly IHealthCheckService _healthCheckService; + + public StartMenu( + IAccountMenu accountMenu, + IStatisticsMenu statisticsMenu, + IHealthCheckService healthCheckService) + { + _accountMenu = accountMenu ?? throw new ArgumentNullException(nameof(accountMenu)); + _statisticsMenu = statisticsMenu ?? throw new ArgumentNullException(nameof(statisticsMenu)); + _healthCheckService = healthCheckService ?? throw new ArgumentNullException(nameof(healthCheckService)); + } + + public async Task PrintAsync(CancellationToken cancellationToken) + { + PrintGreeting(); + + await _healthCheckService.ConnectAsync(); + cancellationToken.ThrowIfCancellationRequested(); + await _healthCheckService.PingAsync(); + + "\nPress any key to show start up menu list.".Print(ConsoleColor.Green); + + Console.ReadKey(); + Console.Clear(); + + await ShowStartAsync(cancellationToken); + } + + private async Task ShowStartAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + "Start menu:".Print(ConsoleColor.DarkYellow); + $"{MenuTypes.Account.GetValue()}. \t{MenuTypes.Account.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + $"{MenuTypes.Leaderboard.GetValue()}. \t{MenuTypes.Leaderboard.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + $"{MenuTypes.Exit.GetValue()}. \t{MenuTypes.Exit.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + + "Please select an item from the list".Print(ConsoleColor.Green); + + Console.Write("Select -> "); + + var menuType = Console.ReadLine().TryGetMenuType(); + + if (menuType is MenuTypes.Unknown) + { + "Invalid input. Try again.".Print(ConsoleColor.Red); + + continue; + } + + switch (menuType) + { + case MenuTypes.Account: + await _accountMenu.StartAsync(cancellationToken); + Console.Clear(); + + break; + + case MenuTypes.Leaderboard: + await _statisticsMenu.StartAsync(cancellationToken); + Console.Clear(); + + break; + + case MenuTypes.Exit: + await _accountMenu.LogoutAsync(cancellationToken); + Console.Clear(); + + return; + + default: + "Invalid input. Try again.".Print(ConsoleColor.Red); + continue; + } + } + } + + private static void PrintGreeting() + { + ("VERSION 2.0\n" + + "Welcome to the world best game ----> Rock-Paper-Scissors!\n" + + "You are given the opportunity to compete with other users in this wonderful game,\n" + + "or if you don’t have anyone to play, don’t worry,\n" + + "you can find a random player or just try your skill with a bot.").Print(ConsoleColor.White); + + "(c)Ihor Volokhovych & Michael Terekhov".Print(ConsoleColor.Cyan); + } +} \ No newline at end of file diff --git a/Client/Client.StartMenu/Services/HealthCheckService.cs b/Client/Client.StartMenu/Services/HealthCheckService.cs new file mode 100644 index 0000000..2a45c0f --- /dev/null +++ b/Client/Client.StartMenu/Services/HealthCheckService.cs @@ -0,0 +1,54 @@ +using RockPaperScissors.Common.Client; +using RockPaperScissors.Common.Extensions; + +namespace Client.StartMenu.Services; + +internal sealed class HealthCheckService : IHealthCheckService +{ + private readonly IClient _client; + private readonly CancellationTokenSource _cancellationTokenSource; + + public HealthCheckService(IClient client, CancellationTokenSource cancellationTokenSource) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _cancellationTokenSource = + cancellationTokenSource ?? throw new ArgumentNullException(nameof(cancellationTokenSource)); + } + + public async Task ConnectAsync() + { + "\nTrying to connect to the server...".Print(ConsoleColor.White); + var result = await _client.GetAsync("/health", _cancellationTokenSource.Token); + + if (result) + { + "Connected to the server".Print(ConsoleColor.Green); + + return; + } + + "Failed to connect. Closing... \nPress any key...".Print(ConsoleColor.Red); + _cancellationTokenSource.Cancel(); + } + + public async Task PingAsync() + { + await Task.Factory.StartNew(async () => + { + "Starting health checks".Print(ConsoleColor.White); + while (!_cancellationTokenSource.IsCancellationRequested) + { + var result = await _client.GetAsync("/health", _cancellationTokenSource.Token); + + if (!result) + { + "\nConnection to the server lost. Stopping...\nPress any key...".Print(ConsoleColor.Red); + + _cancellationTokenSource.Cancel(); + } + + await Task.Delay(1000); + } + }, TaskCreationOptions.LongRunning); + } +} \ No newline at end of file diff --git a/Client/Client.StartMenu/Services/IHealthCheckService.cs b/Client/Client.StartMenu/Services/IHealthCheckService.cs new file mode 100644 index 0000000..a5243b3 --- /dev/null +++ b/Client/Client.StartMenu/Services/IHealthCheckService.cs @@ -0,0 +1,8 @@ +namespace Client.StartMenu.Services; + +public interface IHealthCheckService +{ + Task ConnectAsync(); + + Task PingAsync(); +} \ No newline at end of file diff --git a/Client/Client.Statistics/Client.Statistics.csproj b/Client/Client.Statistics/Client.Statistics.csproj new file mode 100644 index 0000000..bf5f454 --- /dev/null +++ b/Client/Client.Statistics/Client.Statistics.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/Client/Client.Statistics/Enums/MenuTypes.cs b/Client/Client.Statistics/Enums/MenuTypes.cs new file mode 100644 index 0000000..843803c --- /dev/null +++ b/Client/Client.Statistics/Enums/MenuTypes.cs @@ -0,0 +1,9 @@ +namespace Client.Statistics.Enums; + +internal enum MenuTypes +{ + Unknown = 0, + Personal = 1, + All = 2, + Back = 3, +} \ No newline at end of file diff --git a/Client/Client.Statistics/Extensions/EnumExtensions.cs b/Client/Client.Statistics/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..e57576f --- /dev/null +++ b/Client/Client.Statistics/Extensions/EnumExtensions.cs @@ -0,0 +1,39 @@ +using Client.Statistics.Enums; + +namespace Client.Statistics.EnumExtensions; + +internal static class EnumExtensions +{ + internal static string GetDisplayName(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.Back => "Back", + MenuTypes.All => "Top 10 Users", + MenuTypes.Personal => "Personal statistics", + _ => "Unknown", + }; + } + + internal static MenuTypes TryGetMenuType(this string? stringInput) + { + return stringInput switch + { + "3" => MenuTypes.Back, + "2" => MenuTypes.All, + "1" => MenuTypes.Personal, + _ => MenuTypes.Unknown, + }; + } + + internal static int GetValue(this MenuTypes menuTypes) + { + return menuTypes switch + { + MenuTypes.Back => 3, + MenuTypes.All => 2, + MenuTypes.Personal => 1, + _ => 0, + }; + } +} \ No newline at end of file diff --git a/Client/Client.Statistics/Extensions/ServiceCollectionExtensions.cs b/Client/Client.Statistics/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a9b214c --- /dev/null +++ b/Client/Client.Statistics/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Client.Account.Services; +using Client.Account.Services.Interfaces; +using Client.Statistics.Menus; +using Client.Statistics.Services; +using RockPaperScissors.Common.Client; + +namespace Client.Statistics.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IStatisticsService CreateStatisticsService(this IClient client) + { + return new StatisticsService(client); + } + + public static IStatisticsMenu CreateStatisticsMenu(this IStatisticsService statisticsService, IAccountService accountService) + { + return new StatisticsMenu(statisticsService, accountService); + } +} \ No newline at end of file diff --git a/Client/Client.Statistics/Menus/IStatisticsMenu.cs b/Client/Client.Statistics/Menus/IStatisticsMenu.cs new file mode 100644 index 0000000..c7b7ebe --- /dev/null +++ b/Client/Client.Statistics/Menus/IStatisticsMenu.cs @@ -0,0 +1,6 @@ +namespace Client.Statistics.Menus; + +public interface IStatisticsMenu +{ + Task StartAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Client/Client.Statistics/Menus/StatisticsMenu.cs b/Client/Client.Statistics/Menus/StatisticsMenu.cs new file mode 100644 index 0000000..f40225e --- /dev/null +++ b/Client/Client.Statistics/Menus/StatisticsMenu.cs @@ -0,0 +1,163 @@ +using Client.Account.Services; +using Client.Account.Services.Interfaces; +using Client.Statistics.EnumExtensions; +using Client.Statistics.Enums; +using Client.Statistics.Services; +using RockPaperScissors.Common.Extensions; +using RockPaperScissors.Common.Responses; + +namespace Client.Statistics.Menus; + +internal sealed class StatisticsMenu: IStatisticsMenu +{ + private readonly IStatisticsService _statisticsService; + private readonly IAccountService _accountService; + + public StatisticsMenu(IStatisticsService statisticsService, IAccountService accountService) + { + _statisticsService = statisticsService ?? throw new ArgumentNullException(nameof(statisticsService)); + _accountService = accountService ?? throw new ArgumentNullException(nameof(accountService)); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + "Statistics Menu:".Print(ConsoleColor.DarkYellow); + + if (_accountService.IsAuthorized()) + { + $"{MenuTypes.Personal.GetValue()}.\t{MenuTypes.Personal.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + } + + $"{MenuTypes.All.GetValue()}.\t{MenuTypes.All.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + $"{MenuTypes.Back.GetValue()}.\t{MenuTypes.Back.GetDisplayName()}".Print(ConsoleColor.DarkYellow); + + "\nPlease select an item from the list".Print(ConsoleColor.Green); + + Console.Write("Select -> "); + + var menuType = Console.ReadLine().TryGetMenuType(); + + if (menuType is MenuTypes.Unknown) + { + "Invalid input. Try again.".Print(ConsoleColor.Red); + + continue; + } + + switch (menuType) + { + case MenuTypes.All: + Console.Clear(); + await PrintAllStatisticsAsync(cancellationToken); + + break; + + case MenuTypes.Personal: + Console.Clear(); + await PrintPersonalStatisticsAsync(cancellationToken); + + break; + + case MenuTypes.Back: + Console.Clear(); + + return; + + default: + "Invalid input. Try again.".Print(ConsoleColor.Red); + continue; + } + } + } + + private async Task PrintAllStatisticsAsync(CancellationToken cancellationToken) + { + var allStatistics = await _statisticsService.GetAllAsync(cancellationToken); + + if (allStatistics.IsT0) + { + PrintAllStatistics(allStatistics.AsT0); + + return; + } + + allStatistics.AsT1.Message.Print(ConsoleColor.Red); + } + + private async Task PrintPersonalStatisticsAsync(CancellationToken cancellationToken) + { + if (!_accountService.IsAuthorized()) + { + "User in not logged in.".Print(ConsoleColor.Red); + + return; + } + + var user = _accountService.GetUser(); + var personalStatistics = await _statisticsService.GetPersonalAsync(user.GetBearerToken(), cancellationToken); + + if (personalStatistics.IsT0) + { + PrintStatistics(personalStatistics.AsT0, user.Login!); + + return; + } + + personalStatistics.AsT1.Message.Print(ConsoleColor.Red); + } + + private static void PrintAllStatistics(AllStatisticsResponse[] allStatistics) + { + var statisticsSpan = allStatistics.AsSpan(); + + for (var index = 0; index < statisticsSpan.Length; index++) + { + var color = GetColor(index); + $"{index + 1}. User: {statisticsSpan[index].Login}; Score: {statisticsSpan[index].Score}".Print(color); + } + } + + private static void PrintStatistics(PersonalStatisticsResponse allStatistics, string userName) + { + $"Here is your statistics, \"{userName}\" :".Print(ConsoleColor.White); + + "\tMain statistics".Print(ConsoleColor.DarkYellow); + + $"\t{nameof(allStatistics.Wins)}: {allStatistics.Wins}".Print(ConsoleColor.Green); + $"\t{nameof(allStatistics.Draws)}: {allStatistics.Draws}".Print(ConsoleColor.Yellow); + $"\t{nameof(allStatistics.Loss)}: {allStatistics.Loss}".Print(ConsoleColor.Red); + $"\t{nameof(allStatistics.Score)}: {allStatistics.Score}".Print(ConsoleColor.White); + + "\tOther statistics".Print(ConsoleColor.DarkYellow); + + $"\t{allStatistics.TimeSpent} spent time playing".Print(ConsoleColor.White); + $"\t{allStatistics.UsedPaper} times used 'Paper'".Print(ConsoleColor.White); + $"\t{allStatistics.UsedRock} times used 'Rock'".Print(ConsoleColor.White); + $"\t{allStatistics.UsedScissors} times used 'Scissors'".Print(ConsoleColor.White); + $"\t{allStatistics.WinLossRatio}% Win/Loss ratio".Print(GetColor(allStatistics.WinLossRatio)); + } + + private static ConsoleColor GetColor(int index) + { + return index switch + { + < 3 => ConsoleColor.Green, + < 6 => ConsoleColor.Yellow, + _ => ConsoleColor.White + }; + } + + private static ConsoleColor GetColor(double index) + { + return index switch + { + > 75d => ConsoleColor.DarkGreen, + > 50d => ConsoleColor.Green, + > 25d => ConsoleColor.Yellow, + > 10d => ConsoleColor.Red, + _ => ConsoleColor.DarkRed + }; + } +} \ No newline at end of file diff --git a/Client/Client.Statistics/Services/IStatisticsService.cs b/Client/Client.Statistics/Services/IStatisticsService.cs new file mode 100644 index 0000000..aa4fcbe --- /dev/null +++ b/Client/Client.Statistics/Services/IStatisticsService.cs @@ -0,0 +1,14 @@ +using OneOf; +using RockPaperScissors.Common; +using RockPaperScissors.Common.Responses; + +namespace Client.Statistics.Services; + +public interface IStatisticsService +{ + Task> GetAllAsync(CancellationToken cancellationToken); + + Task> GetPersonalAsync( + string? token, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Client/Client.Statistics/Services/StatisticsService.cs b/Client/Client.Statistics/Services/StatisticsService.cs new file mode 100644 index 0000000..7780151 --- /dev/null +++ b/Client/Client.Statistics/Services/StatisticsService.cs @@ -0,0 +1,37 @@ +using OneOf; +using RockPaperScissors.Common; +using RockPaperScissors.Common.Client; +using RockPaperScissors.Common.Responses; + +namespace Client.Statistics.Services; + +internal sealed class StatisticsService: IStatisticsService +{ + private readonly IClient _client; + + public StatisticsService(IClient client) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + } + + public Task> GetAllAsync(CancellationToken cancellationToken) + { + var response = + _client.GetAsync("api/Statistics/all", cancellationToken); + + return response; + } + + public Task> GetPersonalAsync(string? token, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(token)) + { + return Task.FromResult(OneOf.FromT1(new CustomException("Token must not be 'null' or '\"\"'"))); + } + + var response = + _client.GetAsync("api/Statistics/personal", ("Authorization", token), cancellationToken); + + return response; + } +} \ No newline at end of file diff --git a/Client/Client.csproj b/Client/Client.csproj deleted file mode 100644 index 00d2bb4..0000000 --- a/Client/Client.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - - - - - - - - - - diff --git a/Client/Models/Account.cs b/Client/Models/Account.cs deleted file mode 100644 index 5f9de0b..0000000 --- a/Client/Models/Account.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Client.Models.Interfaces; -using Newtonsoft.Json; - -namespace Client.Models -{ - internal class Account : IAccount - { - - [JsonProperty("SessionId")] - public string SessionId { get; set; } - - [JsonProperty("Login")] - public string Login { get; set; } - - [JsonProperty("Password")] - public string Password { get; set; } - - [JsonProperty("LastRequest")] - public DateTime LastRequest { get; set; } - //soon wil be deleted - public override string ToString() - { - return $"Login:\t{Login}\n" + - $"Password:\t{Password}"; - } - } -} diff --git a/Client/Models/Interfaces/IAccount.cs b/Client/Models/Interfaces/IAccount.cs deleted file mode 100644 index 6babd23..0000000 --- a/Client/Models/Interfaces/IAccount.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Client.Models.Interfaces -{ - internal interface IAccount - { - public string SessionId { get; set; } - public string Login { get; set; } - public string Password { get; set; } - public DateTime LastRequest { get; set; } - //soon wil be deleted - } -} diff --git a/Client/Models/Interfaces/IOverallStatistics.cs b/Client/Models/Interfaces/IOverallStatistics.cs deleted file mode 100644 index 54392be..0000000 --- a/Client/Models/Interfaces/IOverallStatistics.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Client.Models.Interfaces -{ - internal interface IOverallStatistics - { - string Login { get; set; } - - int Score { get; set; } - } -} \ No newline at end of file diff --git a/Client/Models/Interfaces/IRoom.cs b/Client/Models/Interfaces/IRoom.cs deleted file mode 100644 index ae76a92..0000000 --- a/Client/Models/Interfaces/IRoom.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Newtonsoft.Json; - -namespace Client.Models.Interfaces -{ - internal interface IRoom - { - [JsonProperty("RoomId")] - string RoomId { get; set; } - - [JsonProperty("Players")] - ConcurrentDictionary Players { get; set; } - - [JsonProperty("CurrentRoundId")] - string CurrentRoundId { get; set; } - - [JsonProperty("CreationTime")] - DateTime CreationTime { get; set; } - bool IsReady { get; set; } - } -} \ No newline at end of file diff --git a/Client/Models/Interfaces/IRound.cs b/Client/Models/Interfaces/IRound.cs deleted file mode 100644 index d2f2602..0000000 --- a/Client/Models/Interfaces/IRound.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Newtonsoft.Json; - -namespace Client.Models.Interfaces -{ - internal interface IRound - { - [JsonProperty("Id")] - string Id { get; init; } //Not to store identical rounds - - [JsonProperty("Moves")] - ConcurrentDictionary PlayerMoves { get; set; } //where string key is playerId - - [JsonProperty("IsFinished")] - bool IsFinished { get; set; } //Probably not needed. - - [JsonProperty("TimeFinished")] - DateTime TimeFinished { get; set; } - - [JsonProperty("WinnerId")] - string WinnerId { get; set; } - - [JsonProperty("LoserId")] - string LoserId { get; set; } - } -} \ No newline at end of file diff --git a/Client/Models/Interfaces/IStatistics.cs b/Client/Models/Interfaces/IStatistics.cs deleted file mode 100644 index e0d4f36..0000000 --- a/Client/Models/Interfaces/IStatistics.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; - -namespace Client.Models.Interfaces -{ - internal interface IStatistics - { - [JsonProperty("Id")] - string Id { get; set; } - - [JsonProperty("UserLogin")] - string Login { get; set; } - - [JsonProperty("Wins")] - int Wins { get; set; } - - [JsonProperty("Loss")] - int Loss { get; set; } - - [JsonProperty("Draws")] - int Draws { get; set; } - - [JsonProperty("WinToLossRatio")] - double WinLossRatio { get; set; } - - [JsonProperty("TimeSpent")] - string TimeSpent { get; set; } - - [JsonProperty("UsedRock")] - int UsedRock { get; set; } - - [JsonProperty("UsedPaper")] - int UsedPaper { get; set; } - - [JsonProperty("UsedScissors")] - int UsedScissors { get; set; } - - [JsonProperty("Score")] - int Score { get; set; } - } -} \ No newline at end of file diff --git a/Client/Models/Interfaces/StatisticsDto.cs b/Client/Models/Interfaces/StatisticsDto.cs deleted file mode 100644 index 8b6d390..0000000 --- a/Client/Models/Interfaces/StatisticsDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; - -namespace Client.Models.Interfaces -{ - public class StatisticsDto - { - [JsonProperty("Login")] - public string Login { get; set; } - - [JsonProperty("Score")] - public int Score { get; set; } - - public override string ToString() - { - return $"Login: {Login} ; Score: {Score}\n"; - } - } -} \ No newline at end of file diff --git a/Client/Models/Room.cs b/Client/Models/Room.cs deleted file mode 100644 index 07e93e8..0000000 --- a/Client/Models/Room.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using Client.Models.Interfaces; - -namespace Client.Models -{ - internal class Room : IRoom - { - [JsonProperty("RoomId")] - public string RoomId { get; set; } - - [JsonProperty("Players")] - public ConcurrentDictionary Players { get; set; } - - [JsonProperty("CurrentRoundId")] - public string CurrentRoundId { get; set; } - - [JsonProperty("CreationTime")] - public DateTime CreationTime { get; set; } - - public bool IsReady { get; set; } - } -} diff --git a/Client/Models/Round.cs b/Client/Models/Round.cs deleted file mode 100644 index 7bfefc7..0000000 --- a/Client/Models/Round.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using Client.Models.Interfaces; - -namespace Client.Models -{ - internal class Round : IRound - { - [JsonProperty("Id")] - public string Id { get; init; } //Not to store identical rounds - - [JsonProperty("Moves")] - public ConcurrentDictionary PlayerMoves { get; set; } //where string key is playerId - - [JsonProperty("IsFinished")] - public bool IsFinished { get; set; } //Probably not needed. - - [JsonProperty("TimeFinished")] - public DateTime TimeFinished { get; set; } - - [JsonProperty("WinnerId")] - public string WinnerId { get; set; } - - [JsonProperty("LoserId")] - public string LoserId { get; set; } - - [JsonProperty("IsDraw")] - public bool IsDraw { get; set; } - } -} \ No newline at end of file diff --git a/Client/Models/Statistics.cs b/Client/Models/Statistics.cs deleted file mode 100644 index 0b4d026..0000000 --- a/Client/Models/Statistics.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Client.Models.Interfaces; - -namespace Client.Models -{ - internal class Statistics : IStatistics, IOverallStatistics - { - public string Id { get; set; } - public string Login { get; set; } - - public int Wins { get; set; } - - public int Loss { get; set; } - - public int Draws { get; set; } - - public double WinLossRatio { get; set; } - - public string TimeSpent { get; set; } - - public int UsedRock { get; set; } - - public int UsedPaper { get; set; } - - public int UsedScissors { get; set; } - public int Score { get; set; } - - public override string ToString() - { - return $"Times Won: {Wins}\n" + - $"Times Lost:{Loss}\n" + - $"Win to Loss ratio: {WinLossRatio}\n" + - $"{TimeSpent}\n" + - $"Times used rock: {UsedRock}\n" + - $"Times used paper: {UsedPaper}\n" + - $"Times used scissors: {UsedScissors}\n" + - $"Total score: {Score}"; - } - } -} \ No newline at end of file diff --git a/Client/Models/StringDestination.cs b/Client/Models/StringDestination.cs deleted file mode 100644 index 4e02c13..0000000 --- a/Client/Models/StringDestination.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Client.Models -{ - public enum StringDestination - { - Login, - Password, - Email, - PassportType //If firstname, lastname...(data without digits!) - } -} diff --git a/Client/Program.cs b/Client/Program.cs deleted file mode 100644 index 99c05fc..0000000 --- a/Client/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Client.Services.RequestProcessor.Impl; -using System; -using System.Threading.Tasks; - -namespace Client -{ - internal static class Program - { - private static async Task Main() - { - try - { - var emulator = new ClientAppEmulator(new RequestPerformer()); - return await emulator.StartAsync(); - } - catch (Exception) //todo : do this need a message? - { - return -1; - } - } - } -} \ No newline at end of file diff --git a/Client/Services/ColorTextWriterService.cs b/Client/Services/ColorTextWriterService.cs deleted file mode 100644 index 14dc144..0000000 --- a/Client/Services/ColorTextWriterService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Client.Services -{ - public static class ColorTextWriterService - { - public static void PrintLineMessageWithSpecialColor(string msg, ConsoleColor color) - { - Console.ForegroundColor = color; - Console.WriteLine(msg); - Console.ResetColor(); - } - - public static void PrintMessageWithSpecialColor(string msg, ConsoleColor color) - { - Console.ForegroundColor = color; - Console.Write(msg); - Console.ResetColor(); - } - } -} diff --git a/Client/Services/RequestProcessor/IRequestHandler.cs b/Client/Services/RequestProcessor/IRequestHandler.cs deleted file mode 100644 index 61e8955..0000000 --- a/Client/Services/RequestProcessor/IRequestHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Client.Services.RequestModels; -using System; -using System.Net.Http; -using System.Threading.Tasks; - -namespace Client.Services.RequestProcessor -{ - public interface IRequestHandler - { - Task HandleRequestAsync(IRequestOptions requestOptions); - } -} diff --git a/Client/Services/RequestProcessor/IRequestPerformer.cs b/Client/Services/RequestProcessor/IRequestPerformer.cs deleted file mode 100644 index a028f21..0000000 --- a/Client/Services/RequestProcessor/IRequestPerformer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Client.Services.RequestModels; -using Client.Services.RequestProcessor.RequestModels.Impl; -using System; -using System.Threading.Tasks; - -namespace Client.Services.RequestProcessor -{ - public interface IRequestPerformer - { - Task PerformRequestAsync(IRequestOptions requestOptions); - } -} diff --git a/Client/Services/RequestProcessor/Impl/RequestHandler.cs b/Client/Services/RequestProcessor/Impl/RequestHandler.cs deleted file mode 100644 index d43634e..0000000 --- a/Client/Services/RequestProcessor/Impl/RequestHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Client.Services.RequestModels; -using Client.Services.RequestProcessor.RequestModels.Impl; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace Client.Services.RequestProcessor.Impl -{ - public class RequestHandler : IRequestHandler - { - private HttpClient _client; - public async Task HandleRequestAsync(IRequestOptions requestOptions) - { - var handler = new HttpClientHandler(); - handler.ServerCertificateCustomValidationCallback += (sender, certificate, chain, errors) => true; - _client = new HttpClient(handler); - if (requestOptions == null) throw new ArgumentNullException(nameof(requestOptions)); - if (!requestOptions.IsValid) throw new ArgumentOutOfRangeException(nameof(requestOptions)); - - using var msg = new HttpRequestMessage(MapMethod(requestOptions.Method), new Uri(requestOptions.Address)); - try - { - if (MapMethod(requestOptions.Method) == HttpMethod.Delete) - { - using var responseD = await _client.SendAsync(msg); - var bodyD = await responseD.Content.ReadAsStringAsync(); - return new Response(true, (int)responseD.StatusCode, bodyD); - } - - if (MapMethod(requestOptions.Method) != HttpMethod.Get) - { - msg.Content = new StringContent(requestOptions.Body, Encoding.UTF8, requestOptions.ContentType); - using var responseForPushingData = await _client.SendAsync(msg); - var bodyForPushing = await responseForPushingData.Content.ReadAsStringAsync(); - return new Response(true, (int)responseForPushingData.StatusCode, bodyForPushing); - } - using var response = await _client.SendAsync(msg); - var body = await response.Content.ReadAsStringAsync(); - return new Response(true, (int)response.StatusCode, body); - } - catch (HttpRequestException) //todo: probably redo - { - return new Response(false, 500, "Server is not responding!"); - } - } - private static HttpMethod MapMethod(RequestMethod method) - { - return method switch - { - RequestMethod.Get => HttpMethod.Get, - RequestMethod.Post => HttpMethod.Post, - RequestMethod.Put => HttpMethod.Put, - RequestMethod.Patch => HttpMethod.Patch, - RequestMethod.Delete => HttpMethod.Delete, - _ => throw new ArgumentOutOfRangeException(nameof(method), method, "Invalid request method") - }; - } - } -} diff --git a/Client/Services/RequestProcessor/Impl/RequestPerformer.cs b/Client/Services/RequestProcessor/Impl/RequestPerformer.cs deleted file mode 100644 index e5b73d5..0000000 --- a/Client/Services/RequestProcessor/Impl/RequestPerformer.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Client.Services.RequestModels; -using Client.Services.RequestProcessor.RequestModels.Impl; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Client.Services.RequestProcessor.Impl -{ - public class RequestPerformer : IRequestPerformer - { - public RequestPerformer() - { - RequestHandler = new RequestHandler(); - } - public readonly IRequestHandler RequestHandler; - public async Task PerformRequestAsync(IRequestOptions requestOptions) - { - IResponse response = null; - if (requestOptions == null) throw new ArgumentNullException(nameof(requestOptions)); - if (!requestOptions.IsValid) throw new ArgumentOutOfRangeException(nameof(requestOptions)); - try - { - response = await RequestHandler.HandleRequestAsync(requestOptions); - } - catch (TimeoutException) //todo: Probably redo - { - response = new Response(false, 408, null); - } - return response; - } - } -} diff --git a/Client/Services/RequestProcessor/RequestModels/IRequestOptions.cs b/Client/Services/RequestProcessor/RequestModels/IRequestOptions.cs deleted file mode 100644 index 5d0e7e1..0000000 --- a/Client/Services/RequestProcessor/RequestModels/IRequestOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Client.Services.RequestModels -{ - public interface IRequestOptions - { - string Name { get; } - string Address { get; } - RequestMethod Method { get; } - string ContentType { get; } - string Body { get; } - bool IsValid { get; } - } -} diff --git a/Client/Services/RequestProcessor/RequestModels/IResponse.cs b/Client/Services/RequestProcessor/RequestModels/IResponse.cs deleted file mode 100644 index 2387385..0000000 --- a/Client/Services/RequestProcessor/RequestModels/IResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Client.Services.RequestModels -{ - public interface IResponse - { - public bool Handled { get; } - public int Code { get; } - public string Content { get; } - } -} \ No newline at end of file diff --git a/Client/Services/RequestProcessor/RequestModels/Impl/RequestOptions.cs b/Client/Services/RequestProcessor/RequestModels/Impl/RequestOptions.cs deleted file mode 100644 index afa9ef9..0000000 --- a/Client/Services/RequestProcessor/RequestModels/Impl/RequestOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Client.Services.RequestModels; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Client.Services.RequestProcessor.RequestModels.Impl -{ - public class RequestOptions : IRequestOptions - { - public string Name { get; set; } - - public string Address { get; set; } - - public RequestMethod Method { get; set; } - - public string ContentType { get; set; } - - public string Body { get; set; } - - public bool IsValid { get; set; } - } -} diff --git a/Client/Services/RequestProcessor/RequestModels/Impl/Response.cs b/Client/Services/RequestProcessor/RequestModels/Impl/Response.cs deleted file mode 100644 index d5703e8..0000000 --- a/Client/Services/RequestProcessor/RequestModels/Impl/Response.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Client.Services.RequestModels; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Client.Services.RequestProcessor.RequestModels.Impl -{ - public class Response : IResponse - { - public Response(bool handled, int code, string content) - { - Handled = handled; - Code = code; - Content = content; - } - public bool Handled { get; set; } - - public int Code { get; set; } - - public string Content { get; set; } - } -} diff --git a/Client/Services/RequestProcessor/RequestModels/RequestMethod.cs b/Client/Services/RequestProcessor/RequestModels/RequestMethod.cs deleted file mode 100644 index 893fe81..0000000 --- a/Client/Services/RequestProcessor/RequestModels/RequestMethod.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Client.Services.RequestModels -{ - public enum RequestMethod - { - Undefined = 0, - Get = 1, - Post = 2, - Put = 3, - Patch = 4, - Delete =5 - } -} diff --git a/Client/Services/StringPlaceholder.cs b/Client/Services/StringPlaceholder.cs deleted file mode 100644 index b7fc5b8..0000000 --- a/Client/Services/StringPlaceholder.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Client.Models; -using Client.Validations; - -namespace Client.Services -{ - class StringPlaceholder - { - private StringDestination destination; - public StringPlaceholder() - { - destination = StringDestination.Login; - } - public StringPlaceholder(StringDestination destination) - { - this.destination = destination; - } - public string BuildNewSpecialDestinationString(string msg, bool isNeedConfirmation = false) - { - string output; - while (true) - { - bool passwordNotConfirmed = true; - if(destination == StringDestination.PassportType || destination == StringDestination.Email) - ColorTextWriterService.PrintLineMessageWithSpecialColor($"What is your {msg}?", ConsoleColor.Yellow); - else - ColorTextWriterService.PrintLineMessageWithSpecialColor($"Try to come up with {msg}?", ConsoleColor.Yellow); - Console.Write($"{msg}--> "); - output = Console.ReadLine() - ?.Trim() - .Replace(" ", ""); - if (String.IsNullOrEmpty(output)) - { - ColorTextWriterService.PrintLineMessageWithSpecialColor("Wrong data!", ConsoleColor.Red); - continue; - } - if(destination == StringDestination.Password && output.Length < 6) - { - ColorTextWriterService.PrintLineMessageWithSpecialColor("Wrong password length!", ConsoleColor.Red); - continue; - } - if (destination == StringDestination.Email && !StringValidator.IsEmailValid(output)) - { - ColorTextWriterService.PrintLineMessageWithSpecialColor("This email is not valid!", ConsoleColor.Red); - continue; - } - if (destination == StringDestination.Password) - { - if (isNeedConfirmation == true) - break; - ColorTextWriterService.PrintLineMessageWithSpecialColor("You need to confirm password!", ConsoleColor.Yellow); - string confirmationPassword; - do - { - Console.Write("Confirmation--> "); - confirmationPassword = Console.ReadLine() - ?.Trim() - .Replace(" ", ""); - if (String.IsNullOrEmpty(output)) - { - ColorTextWriterService.PrintLineMessageWithSpecialColor("Wrong data!", ConsoleColor.Red); - continue; - } - if (output == confirmationPassword) - { - ColorTextWriterService.PrintLineMessageWithSpecialColor("Password confirmed", ConsoleColor.Green); - passwordNotConfirmed = false; - } - else - ColorTextWriterService.PrintLineMessageWithSpecialColor("Passwords dont match!",ConsoleColor.Red); - } while (passwordNotConfirmed); - } - if (destination == StringDestination.PassportType && StringValidator.IsStringContainsDigits(output)) - { - ColorTextWriterService.PrintLineMessageWithSpecialColor("You cannot enter nameType with digits!", ConsoleColor.Red); - continue; - } - break; - } - return output; - } - } -} diff --git a/Client/Validations/StringValidator.cs b/Client/Validations/StringValidator.cs deleted file mode 100644 index a1df9d2..0000000 --- a/Client/Validations/StringValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Client.Validations -{ - public static class StringValidator - { - public static bool IsStringContainsDigits(string str) - { - var res = str.Any(ch => Char.IsDigit(ch)); - return res; - } - public static bool IsEmailValid(string email) - { - var emailPattern = "[.\\-_a-z0-9]+@([a-z0-9][\\-a-z0-9]+\\.)+[a-z]{2,6}"; - var match = Regex.Match(email,emailPattern,RegexOptions.IgnoreCase); - return match.Success; - } - } -} diff --git a/Common/RockPaperScissors.Common/Client/Client.cs b/Common/RockPaperScissors.Common/Client/Client.cs new file mode 100644 index 0000000..6e33849 --- /dev/null +++ b/Common/RockPaperScissors.Common/Client/Client.cs @@ -0,0 +1,169 @@ +using System.Net; +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using OneOf; + +namespace RockPaperScissors.Common.Client; + +public sealed class Client : IClient +{ + private static readonly HttpClient HttpClient = new() + { + BaseAddress = null, + DefaultRequestVersion = HttpVersion.Version20, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower, + DefaultRequestHeaders = + { + {"Accept", MediaTypeNames.Application.Json} + } + }; + + private Client(string baseAddress) + { + HttpClient.BaseAddress = new Uri(baseAddress); + } + + public static Client Create(string baseAddress) => new(baseAddress); + + public async Task> GetAsync( + string url, + CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(HttpClient.BaseAddress!, url), + Version = HttpClient.DefaultRequestVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionOrLower + }; + + using var response = + await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } + + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } + + public async Task> GetAsync( + string url, + (string HeaderKey, string HeaderValue) headerValues, + CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage + { + Method = HttpMethod.Get, + Headers = { + { + headerValues.HeaderKey, headerValues.HeaderValue + } + }, + RequestUri = new Uri(HttpClient.BaseAddress!, url), + Version = HttpClient.DefaultRequestVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionOrLower + }; + + using var response = + await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } + + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } + + public async Task GetAsync( + string url, + CancellationToken cancellationToken) + { + try + { + using var request = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(HttpClient.BaseAddress!, url), + Version = HttpClient.DefaultRequestVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionOrLower + }; + + using var response = + await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + return response.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + public async Task> PostAsync( + string url, + T1 content, + CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, MediaTypeNames.Application.Json), + RequestUri = new Uri(HttpClient.BaseAddress!, url), + Version = HttpClient.DefaultRequestVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionOrLower + }; + + using var response = + await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } + + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } + + public async Task> PostAsync( + string url, + T1 content, + (string HeaderKey, string HeaderValue) headerValues, + CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + Headers = { + { + headerValues.HeaderKey, headerValues.HeaderValue + } + }, + Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, MediaTypeNames.Application.Json), + RequestUri = new Uri(HttpClient.BaseAddress!, url), + Version = HttpClient.DefaultRequestVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionOrLower + }; + + using var response = + await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } + + return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!; + } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Client/IClient.cs b/Common/RockPaperScissors.Common/Client/IClient.cs new file mode 100644 index 0000000..f7f7f1f --- /dev/null +++ b/Common/RockPaperScissors.Common/Client/IClient.cs @@ -0,0 +1,29 @@ +using OneOf; + +namespace RockPaperScissors.Common.Client; + +public interface IClient +{ + Task> GetAsync( + string url, + CancellationToken cancellationToken = default); + + Task> GetAsync( + string url, + (string HeaderKey, string HeaderValue) headerValues, + CancellationToken cancellationToken = default); + + Task GetAsync( + string url, + CancellationToken cancellationToken); + + Task> PostAsync( + string url, + T1 content, + CancellationToken cancellationToken = default); + + Task> PostAsync( + string url, T1 content, + (string HeaderKey, string HeaderValue) headerValues, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/CustomException.cs b/Common/RockPaperScissors.Common/CustomException.cs new file mode 100644 index 0000000..a1fdba4 --- /dev/null +++ b/Common/RockPaperScissors.Common/CustomException.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common; + +public sealed class CustomException +{ + [JsonConstructor] + public CustomException(int code, string message) + { + Message = message; + Code = code; + } + + public CustomException(string message) + { + Message = message; + Code = 400; + } + + [JsonPropertyName("code")] + public int Code { get; init; } + + [JsonPropertyName("message")] + public string Message { get; init; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Enums/Move.cs b/Common/RockPaperScissors.Common/Enums/Move.cs new file mode 100644 index 0000000..bbb657a --- /dev/null +++ b/Common/RockPaperScissors.Common/Enums/Move.cs @@ -0,0 +1,12 @@ +namespace RockPaperScissors.Common.Enums; + +public enum Move +{ + None = 0, + + Rock = 1, + + Paper = 2, + + Scissors = 3, +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Enums/PlayerState.cs b/Common/RockPaperScissors.Common/Enums/PlayerState.cs new file mode 100644 index 0000000..8b3ca7a --- /dev/null +++ b/Common/RockPaperScissors.Common/Enums/PlayerState.cs @@ -0,0 +1,12 @@ +namespace RockPaperScissors.Common.Enums; + +public enum PlayerState +{ + None = 0, + + Lose = 1, + + Win = 2, + + Draw = 3, +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Enums/StringDestination.cs b/Common/RockPaperScissors.Common/Enums/StringDestination.cs new file mode 100644 index 0000000..3dec296 --- /dev/null +++ b/Common/RockPaperScissors.Common/Enums/StringDestination.cs @@ -0,0 +1,9 @@ +namespace RockPaperScissors.Common.Enums; + +public enum StringDestination +{ + Login, + Password, + Email, + PassportType //If firstname, lastname...(data without digits!) +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Extensions/QueryBuilder.cs b/Common/RockPaperScissors.Common/Extensions/QueryBuilder.cs new file mode 100644 index 0000000..adaa602 --- /dev/null +++ b/Common/RockPaperScissors.Common/Extensions/QueryBuilder.cs @@ -0,0 +1,133 @@ +using System.Buffers; +using System.Collections; + +namespace RockPaperScissors.Common.Extensions; + +public sealed class QueryBuilder : IReadOnlyCollection> +{ + private readonly IList> _valuePairs; + + /// + public int Count { get; private set; } + + /// + /// Constructor. + /// + public QueryBuilder() + { + _valuePairs = new List>(); + } + + /// + /// Constructor. + /// + public QueryBuilder(int count) + { + Count = count; + _valuePairs = new List>(count); + } + + /// + /// Constructor. + /// + /// Collection of KeyValuePairs. + public QueryBuilder(IReadOnlyCollection> parameters) + { + Count += parameters.Sum(pair => pair.Key.Length + pair.Value.Length); + + _valuePairs = new List>(parameters); + } + + /// + /// Adds key and it's values to the collection. + /// + /// Key. + /// Collection of values. + public void Add(string key, IEnumerable values) + { + Count += key.Length; + + foreach (var value in values) + { + Count += value.Length; + + _valuePairs.Add(KeyValuePair.Create(key, value)); + } + } + + /// + /// Adds key and it's value to the collection. + /// + /// Key. + /// Value. + public void Add(string key, string? value) + { + Count += key.Length + value.Length; + + _valuePairs.Add(KeyValuePair.Create(key, value)); + } + + /// + public override string ToString() + { + if (Count is 0) + { + return string.Empty; + } + + var queryLength = Count * 2; + + var isStackAlloc = queryLength <= 64; + var currentPosition = 0; + + var array = isStackAlloc ? null : ArrayPool.Shared.Rent(queryLength); + var resultSpan = isStackAlloc ? stackalloc char[queryLength] : array; + + try + { + var first = true; + for (var i = 0; i < _valuePairs.Count; i++) + { + var pair = _valuePairs[i]; + resultSpan[currentPosition] = first ? '?' : '&'; + first = false; + currentPosition++; + + var escapeKey = Uri.EscapeDataString(pair.Key); + escapeKey.CopyTo(resultSpan[currentPosition..]); + currentPosition += escapeKey.Length; + + resultSpan[currentPosition++] = '='; + + var escapedValue = Uri.EscapeDataString(pair.Value); + escapedValue.CopyTo(resultSpan[currentPosition..]); + currentPosition += escapedValue.Length; + } + + var endIndex = resultSpan.IndexOf('\0'); + + return endIndex is -1 + ? resultSpan[..currentPosition].ToString() + : resultSpan[..(endIndex > currentPosition ? currentPosition : endIndex)].ToString(); + } + finally + { + if (array is not null) + { + ArrayPool.Shared.Return(array); + } + } + } + + /// + public IEnumerator> GetEnumerator() + { + return _valuePairs.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _valuePairs.GetEnumerator(); + } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Extensions/TextWriteExtensions.cs b/Common/RockPaperScissors.Common/Extensions/TextWriteExtensions.cs new file mode 100644 index 0000000..9038de9 --- /dev/null +++ b/Common/RockPaperScissors.Common/Extensions/TextWriteExtensions.cs @@ -0,0 +1,108 @@ +using System.Text.RegularExpressions; +using RockPaperScissors.Common.Enums; + +namespace RockPaperScissors.Common.Extensions; + +public static class TextWriteExtensions +{ + private static readonly Regex EmailRegex = + new("[.\\-_a-z0-9]+@([a-z0-9][\\-a-z0-9]+\\.)+[a-z]{2,6}", RegexOptions.Compiled); + + public static void Print(this string message, ConsoleColor color) + { + Console.ForegroundColor = color; + Console.WriteLine(message); + Console.ResetColor(); + } + + public static string BuildString(this string msg, StringDestination destination, bool isNeedConfirmation = false) + { + string output; + while (true) + { + var passwordNotConfirmed = true; + + Print( + destination is StringDestination.PassportType or StringDestination.Email + ? $"What is your {msg}?" + : $"Try to come up with {msg}", ConsoleColor.Yellow); + + Console.Write($"{msg}--> "); + + output = Console.ReadLine() + ?.Trim() + .Replace(" ", "") ?? string.Empty; + + if (string.IsNullOrEmpty(output)) + { + Print("Wrong data!", ConsoleColor.Red); + continue; + } + + switch (destination) + { + case StringDestination.Password when output.Length < 6: + Print("Wrong password length!", ConsoleColor.Red); + continue; + case StringDestination.Email when !IsEmailValid(output): + Print("This email is not valid!", ConsoleColor.Red); + continue; + } + + if (destination is StringDestination.Password) + { + if (isNeedConfirmation) + { + break; + } + + Print("You need to confirm password!", ConsoleColor.Yellow); + do + { + Console.Write("Confirmation--> "); + var confirmationPassword = Console.ReadLine() + ?.Trim() + .Replace(" ", ""); + + if (string.IsNullOrEmpty(output)) + { + Print("Wrong data!", ConsoleColor.Red); + continue; + } + + if (output != confirmationPassword) + { + Print("Passwords don't match!",ConsoleColor.Red); + continue; + } + + Print("Password confirmed", ConsoleColor.Green); + passwordNotConfirmed = false; + } + while (passwordNotConfirmed); + } + if (destination is StringDestination.PassportType && ContainsDigits(output)) + { + Print("You cannot enter nameType with digits!", ConsoleColor.Red); + continue; + } + break; + } + + return output; + } + + public static bool ContainsDigits(this string str) + { + var res = str.Any(char.IsDigit); + + return res; + } + + public static bool IsEmailValid(this string email) + { + var match = EmailRegex.Match(email); + + return match.Success; + } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Models/AccountDto.cs b/Common/RockPaperScissors.Common/Models/AccountDto.cs new file mode 100644 index 0000000..f3c0f92 --- /dev/null +++ b/Common/RockPaperScissors.Common/Models/AccountDto.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common.Models; + +public sealed class AccountDto +{ + [JsonPropertyName("login")] + [Required(ErrorMessage = "Login is required!")] + public string Login { get; init; } + + [JsonPropertyName("password")] + [Required(ErrorMessage = "Password is required!")] + [StringLength(20, MinimumLength = 6, ErrorMessage = "Invalid password length")] + public string Password { get; init;} +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Models/StatisticsDto.cs b/Common/RockPaperScissors.Common/Models/StatisticsDto.cs new file mode 100644 index 0000000..b0a60f1 --- /dev/null +++ b/Common/RockPaperScissors.Common/Models/StatisticsDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common.Models; + +public sealed class StatisticsDto +{ + [JsonPropertyName("login")] + public string Login { get; init; } + + [JsonPropertyName("score")] + public int Score { get; init; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Requests/LoginRequest.cs b/Common/RockPaperScissors.Common/Requests/LoginRequest.cs new file mode 100644 index 0000000..71891b3 --- /dev/null +++ b/Common/RockPaperScissors.Common/Requests/LoginRequest.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common.Requests; + +public sealed class LoginRequest +{ + [JsonPropertyName("login")] + [Required(ErrorMessage = "Login is required!")] + public string Login { get; init; } + + [JsonPropertyName("password")] + [Required(ErrorMessage = "Password is required!!")] + [StringLength(20, MinimumLength = 6, ErrorMessage = "Invalid password length")] + public string Password { get; init; } + + [JsonPropertyName("lastRequestTime")] + public DateTimeOffset LastRequestTime { get; init; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Requests/MakeMoveRequest.cs b/Common/RockPaperScissors.Common/Requests/MakeMoveRequest.cs new file mode 100644 index 0000000..a825a99 --- /dev/null +++ b/Common/RockPaperScissors.Common/Requests/MakeMoveRequest.cs @@ -0,0 +1,10 @@ +using RockPaperScissors.Common.Enums; + +namespace RockPaperScissors.Common.Requests; + +public sealed class MakeMoveRequest +{ + public string RoundId { get; init; } + + public Move Move { get; init; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Requests/RegisterRequest.cs b/Common/RockPaperScissors.Common/Requests/RegisterRequest.cs new file mode 100644 index 0000000..171d2e4 --- /dev/null +++ b/Common/RockPaperScissors.Common/Requests/RegisterRequest.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common.Requests; + +public sealed class RegisterRequest +{ + [JsonPropertyName("login")] + [Required(ErrorMessage = "Login is required!")] + public string Login { get; init; } + + [JsonPropertyName("password")] + [Required(ErrorMessage = "Password is required!")] + [StringLength(20, MinimumLength = 6, ErrorMessage = "Invalid password length. Must be 6-20")] + public string Password { get; init; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Responses/AllStatisticsResponse.cs b/Common/RockPaperScissors.Common/Responses/AllStatisticsResponse.cs new file mode 100644 index 0000000..c8977c2 --- /dev/null +++ b/Common/RockPaperScissors.Common/Responses/AllStatisticsResponse.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common.Responses; + +public sealed class AllStatisticsResponse +{ + [JsonPropertyName("login")] + public string Login { get; init; } + + /// + /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points. + /// + [JsonPropertyName("score")] + public int Score { get; init; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Responses/LoginResponse.cs b/Common/RockPaperScissors.Common/Responses/LoginResponse.cs new file mode 100644 index 0000000..d7d1802 --- /dev/null +++ b/Common/RockPaperScissors.Common/Responses/LoginResponse.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common.Responses; + +public sealed class LoginResponse +{ + /// + /// Gets or sets user token (used in header). + /// + [JsonPropertyName("token")] + public string Token { get; init; } + + /// + /// Gets or sets user login. + /// + [JsonPropertyName("login")] + public string Login { get; init; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/Responses/PersonalStatisticsResponse.cs b/Common/RockPaperScissors.Common/Responses/PersonalStatisticsResponse.cs new file mode 100644 index 0000000..fce1513 --- /dev/null +++ b/Common/RockPaperScissors.Common/Responses/PersonalStatisticsResponse.cs @@ -0,0 +1,60 @@ +using System.Text.Json.Serialization; + +namespace RockPaperScissors.Common.Responses; + +public sealed class PersonalStatisticsResponse +{ + /// + /// Total amount of Wins + /// + [JsonPropertyName("wins")] + public int Wins { get; set; } + + /// + /// Total amount of Loses + /// + [JsonPropertyName("loss")] + public int Loss { get; set; } + + /// + /// Total amount of Draws. OBSOLETE + /// + [JsonPropertyName("draws")] + public int Draws { get; set; } + + /// + /// Ratio Wins to Losses. Win/Loss * 100 + /// + [JsonPropertyName("winLossRatio")] + public double WinLossRatio { get; set; } + + /// + /// Ratio for the last 7 days + /// + [JsonPropertyName("timeSpent")] + public string TimeSpent { get; set; } + + /// + /// Times used rock + /// + [JsonPropertyName("usedRock")] + public int UsedRock { get; set; } + + /// + /// Times used Paper + /// + [JsonPropertyName("usedPaper")] + public int UsedPaper { get; set; } + + /// + /// Times used Scissors + /// + [JsonPropertyName("usedScissors")] + public int UsedScissors { get; set; } + + /// + /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points. + /// + [JsonPropertyName("score")] + public int Score { get; set; } +} \ No newline at end of file diff --git a/Common/RockPaperScissors.Common/RockPaperScissors.Common.csproj b/Common/RockPaperScissors.Common/RockPaperScissors.Common.csproj new file mode 100644 index 0000000..09d3722 --- /dev/null +++ b/Common/RockPaperScissors.Common/RockPaperScissors.Common.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/Common/RockPaperScissors.Common/UrlTemplates.cs b/Common/RockPaperScissors.Common/UrlTemplates.cs new file mode 100644 index 0000000..b278632 --- /dev/null +++ b/Common/RockPaperScissors.Common/UrlTemplates.cs @@ -0,0 +1,28 @@ +namespace RockPaperScissors.Common; + +public static class UrlTemplates +{ + // Account-related + public const string RegisterUrl = "api/account/register"; + public const string LoginUrl = "api/account/login"; + public const string LogoutUrl = "api/account/logout"; + + // Statistics-related + public const string AllStatistics = "api/statistics/all"; + public const string PersonalStatistics = "api/statistics/personal"; + + // Room-related + public const string CreateRoom = "api/room/create"; + public const string JoinPublicRoom = "api/room/public/join"; + public const string JoinPrivateRoom = "api/room/private/{roomCode}/join"; + public const string UpdateRoom = "api/room/{roomId}/update"; + public const string DeleteRoom = "api/room/{roomId}/delete"; + public const string ChangeStatus = "api/room/{roomId}/playerstatus"; + public const string CheckRoomUpdateTicks = "api/room/{roomId}/status"; + + // Round-related + public const string CreateRound = "api/round/create"; + public const string MakeMove = "api/round/{roundId}/move/{move}"; + public const string UpdateRound = "api/round/{roundId}/update"; + public const string CheckRoundUpdateTicks = "api/round/{roundId}/status"; +} \ No newline at end of file diff --git a/README.md b/README.md index 924f974..98e44a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # RockPaperScissorsGame -This is second project of Parimatch Tech Academy .NET course. +This is second project of PariMatch Tech Academy .NET course. [Technical requirement](https://docs.google.com/document/d/13lsfyUkMJBZAiXt8OlBWYdKq9zExdHdDhBBH00s5tv4/edit?usp=sharing) -[Table of progress](https://docs.google.com/spreadsheets/d/1h2YSv9YhTiELJAsEIE5oPusIhlloKiyxRdfDVqGc_JI/edit?usp=sharing) +~~[Table of progress](https://docs.google.com/spreadsheets/d/1h2YSv9YhTiELJAsEIE5oPusIhlloKiyxRdfDVqGc_JI/edit?usp=sharing)~~ + +[Trello Kanban](https://trello.com/b/vofn5Dhn/rock-paper-scissors) diff --git a/RockPaperScissorsGame.sln b/RockPaperScissorsGame.sln index 8d353c0..3a2d5c2 100644 --- a/RockPaperScissorsGame.sln +++ b/RockPaperScissorsGame.sln @@ -3,9 +3,29 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{53166F83-4032-4CCF-B172-C2517BFBA424}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{B6D69162-F9E0-4165-A288-00FF1D073291}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{540EE2A5-907D-4E6A-A051-22D01DBFE913}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Host", "Server\Server.Host\Server.Host.csproj", "{DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Data", "Server\Server.Data\Server.Data.csproj", "{BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Bll", "Server\Server.Bll\Server.Bll.csproj", "{74D55924-777B-4FC3-B8F5-37508B2EBAE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Authentication", "Server\Server.Authentication\Server.Authentication.csproj", "{1FC79883-C4C0-4C61-A3F1-D699F7A793B2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Host", "Client\Client.Host\Client.Host.csproj", "{45AC8461-58BE-4675-AD96-89CF88CF8CA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Account", "Client\Client.Account\Client.Account.csproj", "{32045C10-4395-4010-A365-6CAF4D993680}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.StartMenu", "Client\Client.StartMenu\Client.StartMenu.csproj", "{BB2630C0-3F25-49F1-9393-4B75A7014187}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RockPaperScissors.Common", "Common\RockPaperScissors.Common\RockPaperScissors.Common.csproj", "{4E6B589E-C9C5-4191-B435-6FE4283C1B58}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Statistics", "Client\Client.Statistics\Client.Statistics.csproj", "{994CC558-A9A9-4E58-A894-EE06993DAC58}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.MainMenu", "Client\Client.MainMenu\Client.MainMenu.csproj", "{3928C9ED-4F7C-4D3E-8265-C98EBC85F318}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -13,14 +33,46 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {53166F83-4032-4CCF-B172-C2517BFBA424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {53166F83-4032-4CCF-B172-C2517BFBA424}.Debug|Any CPU.Build.0 = Debug|Any CPU - {53166F83-4032-4CCF-B172-C2517BFBA424}.Release|Any CPU.ActiveCfg = Release|Any CPU - {53166F83-4032-4CCF-B172-C2517BFBA424}.Release|Any CPU.Build.0 = Release|Any CPU - {B6D69162-F9E0-4165-A288-00FF1D073291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B6D69162-F9E0-4165-A288-00FF1D073291}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B6D69162-F9E0-4165-A288-00FF1D073291}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B6D69162-F9E0-4165-A288-00FF1D073291}.Release|Any CPU.Build.0 = Release|Any CPU + {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Release|Any CPU.Build.0 = Release|Any CPU + {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Release|Any CPU.Build.0 = Release|Any CPU + {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Release|Any CPU.Build.0 = Release|Any CPU + {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Release|Any CPU.Build.0 = Release|Any CPU + {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Release|Any CPU.Build.0 = Release|Any CPU + {32045C10-4395-4010-A365-6CAF4D993680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32045C10-4395-4010-A365-6CAF4D993680}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32045C10-4395-4010-A365-6CAF4D993680}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32045C10-4395-4010-A365-6CAF4D993680}.Release|Any CPU.Build.0 = Release|Any CPU + {BB2630C0-3F25-49F1-9393-4B75A7014187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB2630C0-3F25-49F1-9393-4B75A7014187}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB2630C0-3F25-49F1-9393-4B75A7014187}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB2630C0-3F25-49F1-9393-4B75A7014187}.Release|Any CPU.Build.0 = Release|Any CPU + {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Release|Any CPU.Build.0 = Release|Any CPU + {994CC558-A9A9-4E58-A894-EE06993DAC58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {994CC558-A9A9-4E58-A894-EE06993DAC58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {994CC558-A9A9-4E58-A894-EE06993DAC58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {994CC558-A9A9-4E58-A894-EE06993DAC58}.Release|Any CPU.Build.0 = Release|Any CPU + {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -28,4 +80,15 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4ADC353B-52B1-43B2-B26D-3279A6ACC978} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6} + {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6} + {74D55924-777B-4FC3-B8F5-37508B2EBAE9} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6} + {1FC79883-C4C0-4C61-A3F1-D699F7A793B2} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6} + {45AC8461-58BE-4675-AD96-89CF88CF8CA3} = {540EE2A5-907D-4E6A-A051-22D01DBFE913} + {32045C10-4395-4010-A365-6CAF4D993680} = {540EE2A5-907D-4E6A-A051-22D01DBFE913} + {BB2630C0-3F25-49F1-9393-4B75A7014187} = {540EE2A5-907D-4E6A-A051-22D01DBFE913} + {994CC558-A9A9-4E58-A894-EE06993DAC58} = {540EE2A5-907D-4E6A-A051-22D01DBFE913} + {3928C9ED-4F7C-4D3E-8265-C98EBC85F318} = {540EE2A5-907D-4E6A-A051-22D01DBFE913} + EndGlobalSection EndGlobal diff --git a/Server/Account b/Server/Account deleted file mode 100644 index abbc85c..0000000 --- a/Server/Account +++ /dev/null @@ -1 +0,0 @@ -{"d3441061-4c3d-43ca-8aef-b733525ecd22":{"Id":"d3441061-4c3d-43ca-8aef-b733525ecd22","Login":"123","Password":"123123"}} \ No newline at end of file diff --git a/Server/Contracts/AccountDto.cs b/Server/Contracts/AccountDto.cs deleted file mode 100644 index bec930a..0000000 --- a/Server/Contracts/AccountDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace Server.Contracts -{ - public class AccountDto - { - public string SessionId { get; set; } - - [Required(ErrorMessage = "Login is required!")] - public string Login { get; init; } - [Required(ErrorMessage = "Password is required!!")] - [StringLength(20, MinimumLength=6, ErrorMessage = "Invalid password length")] - public string Password { get; init; } - public DateTime LastRequest { get; set; } - } -} \ No newline at end of file diff --git a/Server/Contracts/StatisticsDto.cs b/Server/Contracts/StatisticsDto.cs deleted file mode 100644 index 0770d11..0000000 --- a/Server/Contracts/StatisticsDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Newtonsoft.Json; - -namespace Server.Contracts -{ - public class StatisticsDto - { - [JsonProperty("Login")] - public string Login { get; set; } - - [JsonProperty("Score")] - public int Score { get; set; } - } -} \ No newline at end of file diff --git a/Server/Controllers/RoomController.cs b/Server/Controllers/RoomController.cs deleted file mode 100644 index 8b95afa..0000000 --- a/Server/Controllers/RoomController.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Server.GameLogic.LogicServices; -using Server.GameLogic.Models.Impl; -using System; -using System.Net; -using System.Net.Mime; -using System.Threading.Tasks; - -namespace Server.Controllers -{ - [ApiController] - [Route("/room")] - [Produces(MediaTypeNames.Application.Json)] - public class RoomController : ControllerBase - { - private readonly IRoomCoordinator _roomManager; - - public RoomController( - IRoomCoordinator roomManager) - { - _roomManager = roomManager; - } - - [HttpPost] - [Route("create/{sessionId}&{isPrivate}")] - [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> CreateRoom(string sessionId, bool isPrivate) - { - try - { - var resultRoom = await _roomManager.CreateRoom(sessionId, isPrivate); - if (resultRoom != null) - { - return resultRoom; - } - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - - } - - [HttpPost] - [Route("join/{sessionId}&{roomId}")] - [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> JoinPrivateRoom(string sessionId, string roomId) - { - try - { - var resultRoom = await _roomManager.JoinPrivateRoom(sessionId, roomId); - - if (resultRoom != null) - { - return resultRoom; - } - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - } - - [HttpGet] - [Route("join/{sessionId}")] - [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> JoinPublicRoom(string sessionId) - { - try - { - var resultRoom = await _roomManager.JoinPublicRoom(sessionId); - - if (resultRoom != null) - { - return resultRoom; - } - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - - } - - [HttpPut] - [Route("updateState/{sessionId}&{state}")] - [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> UpdatePlayerState(string sessionId, bool state) - { - try - { - var resultRound = await _roomManager.UpdatePlayerStatus(sessionId, state); - if (resultRound != null) - return resultRound; - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - } - - [HttpGet] - [Route("updateState/{roomId}")] - [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> Update(string roomId) - { - try - { - var resultRound = await _roomManager.UpdateRoom(roomId); - return resultRound; - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - } - - [HttpGet] - [Route("create/training/{sessionId}")] - [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> CreateTrainingRoom(string sessionId) - { - try - { - var resultRound = await _roomManager.CreateTrainingRoom(sessionId); - return resultRound; - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - } - [HttpDelete] - [Route("delete/{roomId}")] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)] //Probably set new HttpStatus - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> DeleteRoomExit(string roomId) - { - try - { - var deleted = await _roomManager.DeleteRoom(roomId); - if (deleted) - return Ok("Room was deleted successfully!"); - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - } - } -} diff --git a/Server/Controllers/RoundController.cs b/Server/Controllers/RoundController.cs deleted file mode 100644 index 66cf2b8..0000000 --- a/Server/Controllers/RoundController.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Server.GameLogic.LogicServices; -using Server.GameLogic.Models.Impl; -using System; -using System.Net; -using System.Net.Mime; -using System.Threading.Tasks; - -namespace Server.Controllers -{ - [ApiController] - [Route("/round")] - [Produces(MediaTypeNames.Application.Json)] - public class RoundController:ControllerBase - { - public RoundController( - IRoundCoordinator roundManager, - ILogger logger) - { - _roundManager = roundManager; - _logger = logger; - } - [HttpGet] - [Route("get/{roomId}")] - [ProducesResponseType(typeof(Round), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> GetActualActiveRoundForCurrentRoom(string roomId) - { - try - { - var resultedRound = await _roundManager.GetCurrentActiveRoundForSpecialRoom(roomId); - if (resultedRound != null) - { - return resultedRound; - } - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - - } - [HttpGet] - [Route("get/update/{roomId}")] - [ProducesResponseType(typeof(Round), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> UpdateCurrentRound(string roomId) - { - try - { - var resultedRound = await _roundManager.UpdateRound(roomId); - - if (resultedRound != null) - { - return resultedRound; - } - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - - } - - [HttpPatch] - [Route("move/{roomId}&{sessionId}&{move}")] - [ProducesResponseType(typeof(Round), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task> PlaceYourMoveToActiveRound(string roomId, string sessionId, int move) - { - try - { - var resultedRound = await _roundManager.MakeMove(roomId,sessionId,move); - if (resultedRound != null) - { - return resultedRound; - } - return BadRequest(); - } - catch (Exception exception) - { - return BadRequest(exception.Message); - } - - } - - private readonly IRoundCoordinator _roundManager; - private readonly ILogger _logger; - } -} diff --git a/Server/Controllers/StatisticsController.cs b/Server/Controllers/StatisticsController.cs deleted file mode 100644 index 7f48ca4..0000000 --- a/Server/Controllers/StatisticsController.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Server.Contracts; -using Server.Mappings; -using Server.Models; -using Server.Services.Interfaces; - -namespace Server.Controllers -{ - - [ApiController] - [Route("/statistics")] - public class OverallStatisticsController : ControllerBase - { - private readonly IDeserializedObject _deserializedStatistics; - private readonly IAccountManager _accountManager; - private readonly ILogger _logger; //todo: add somewhere logger - - - public OverallStatisticsController( - IDeserializedObject deserializedStatistics, - IAccountManager accountManager, - ILogger logger) - { - _deserializedStatistics = deserializedStatistics; - _accountManager = accountManager; - _logger = logger; - } - - [HttpGet] - [Route("overallStatistics")] - [ProducesResponseType(typeof(IEnumerable), (int) HttpStatusCode.OK)] - [ProducesResponseType((int) HttpStatusCode.BadRequest)] - public ActionResult> GetOverallStatistics() - { - try - { - var deserialized = _deserializedStatistics.ConcurrentDictionary.Values; - - var statisticsList = deserialized.Where(x => x.Score > 10).AsParallel().ToArray(); - - if (statisticsList.Length < 1) - return NotFound(); - - var resultList = statisticsList.Select(statistics => statistics.ToStatisticsDto()).ToList(); - - return resultList; - } - catch (Exception exceptions) //todo: custom exception OR NOT :) - { - return BadRequest(exceptions.Message); - } - - } - - - [HttpGet] - [Route("personalStatistics/{sessionId}")] - [ProducesResponseType(typeof(Statistics), (int) HttpStatusCode.OK)] - [ProducesResponseType((int) HttpStatusCode.BadRequest)] - public ActionResult> GetPersonalStatistics(string sessionId) - { - try - { - var deserialized = _deserializedStatistics.ConcurrentDictionary.Values; - - var thisUserId = _accountManager.AccountsActive - .FirstOrDefault(x => x.Key.Equals(sessionId)).Value.Id; - - var resultStatistics = deserialized.FirstOrDefault(x => x.Id.Equals(thisUserId)); - - return Ok(resultStatistics); - } - catch (Exception exceptions) //todo: custom exception - { - return BadRequest(exceptions.Message); - } - - } - } -} \ No newline at end of file diff --git a/Server/Controllers/UserController.cs b/Server/Controllers/UserController.cs deleted file mode 100644 index 71becc4..0000000 --- a/Server/Controllers/UserController.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Net.Mime; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Server.Contracts; -using Server.Exceptions.LogIn; -using Server.Exceptions.Register; -using Server.GameLogic.LogicServices; -using Server.Models; -using Server.Services.Interfaces; - -namespace Server.Controllers -{ - [ApiController] - [Route ("/user")] - - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - public class UserController : ControllerBase - { - private readonly IStorage _accountStorage; - - private readonly IStorage _statisticsStorage; //Just to write new statistics field into file along with account - - private readonly ILogger _logger; - - private readonly IAccountManager _accountManager; - public UserController( - IStorage users, - IStorage statisticsStorage, - IAccountManager accountManager, - ILogger logger, - IRoomCoordinator roomCoordinator - ) - { - _accountStorage = users; - _statisticsStorage = statisticsStorage; - _accountManager = accountManager; - _logger = logger; - } - /// - /// Method to log in an account - /// - /// Data Transfer Object of account - /// Status code and response string - [HttpPost] - [Route("login")] - [ProducesResponseType(typeof(string),(int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string),(int)HttpStatusCode.BadRequest)] - public async Task> Login(AccountDto accountDto) - { - try - { - await _accountManager.LogInAsync(accountDto); - return Ok($"Signed In as {accountDto.Login}"); - - } - catch (ValidationException exception) - { - _logger.Log(LogLevel.Warning,"Validation error"); - return BadRequest(exception.Message); - } - catch (LoginErrorException exception) - { - _logger.Log(LogLevel.Error,exception.Message); - return BadRequest(exception.Message); - } - - } - - /// - /// Method to register a new account - /// - /// Data Transfer Object of account - /// HttpStatusCode and response string - [HttpPost] - [Route("create")] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.Created)] - [ProducesResponseType(typeof(string),(int)HttpStatusCode.BadRequest)] - public async Task> CreateAccount(AccountDto accountDto) - { - try - { - var account = new Account - { - Id = Guid.NewGuid().ToString(), - Login = accountDto.Login, - Password = accountDto.Password - }; - var statistics = new Statistics - { - Id = account.Id, - Login = account.Login, - }; - - await _accountStorage.AddAsync(account); - await _statisticsStorage.AddAsync(statistics); - - return Created("", $"Account [{account.Login}] successfully created"); - } - catch (ValidationException exception) - { - _logger.Log(LogLevel.Warning,exception.Message); - return BadRequest(exception.Message); - } - catch (UnknownReasonException exception) - { - _logger.Log(LogLevel.Warning, exception.Message); - return BadRequest(exception.Message); - } - } - - /// - /// Method to Log out of account - /// - /// Session id of a client - /// HttpStatusCode - [Route("logout")] - [HttpGet("logout/{sessionId}")] - [ProducesResponseType(typeof(int), (int) HttpStatusCode.OK)] - [ProducesResponseType(typeof(int), (int) HttpStatusCode.BadRequest)] - public async Task> LogOut(string sessionId) - { - var result = await _accountManager.LogOutAsync(sessionId); - if (result) - return (int) HttpStatusCode.OK; - return (int) HttpStatusCode.Forbidden; - } - } -} \ No newline at end of file diff --git a/Server/Exceptions/LogIn/InvalidCredentialsException.cs b/Server/Exceptions/LogIn/InvalidCredentialsException.cs deleted file mode 100644 index 3cfba50..0000000 --- a/Server/Exceptions/LogIn/InvalidCredentialsException.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Server.Exceptions.LogIn -{ - public class InvalidCredentialsException : LoginErrorException - { - protected InvalidCredentialsException() - { - - } - - public InvalidCredentialsException(string message) : - base($"Invalid username of password!") - { - } - - } -} \ No newline at end of file diff --git a/Server/Exceptions/LogIn/LoginCooldownException.cs b/Server/Exceptions/LogIn/LoginCooldownException.cs deleted file mode 100644 index 6adc778..0000000 --- a/Server/Exceptions/LogIn/LoginCooldownException.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Server.Exceptions.LogIn -{ - public class LoginCooldownException : LoginErrorException - { - protected LoginCooldownException() - { - - } - - public LoginCooldownException(string message, int afterTime) : - base($"You got a cooldown! Try in {afterTime} seconds") - { - - } - } -} \ No newline at end of file diff --git a/Server/Exceptions/LogIn/LoginErrorException.cs b/Server/Exceptions/LogIn/LoginErrorException.cs deleted file mode 100644 index 2051446..0000000 --- a/Server/Exceptions/LogIn/LoginErrorException.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Server.Exceptions.LogIn -{ - public class LoginErrorException : Exception - { - protected LoginErrorException() - { - - } - - protected LoginErrorException(string message) : - base(message) - { - - } - - public LoginErrorException(string message, Exception inner) - : base(message, inner) - { - - } - } -} \ No newline at end of file diff --git a/Server/Exceptions/LogIn/UserAlreadySignedInException.cs b/Server/Exceptions/LogIn/UserAlreadySignedInException.cs deleted file mode 100644 index 3c859f0..0000000 --- a/Server/Exceptions/LogIn/UserAlreadySignedInException.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Server.Exceptions.LogIn -{ - public class UserAlreadySignedInException : LoginErrorException - { - public UserAlreadySignedInException() - { - - } - - public UserAlreadySignedInException(string message) : base(string.Format($"Already signed in!")) - { - - } - } -} \ No newline at end of file diff --git a/Server/Exceptions/LogIn/UserNotFoundException.cs b/Server/Exceptions/LogIn/UserNotFoundException.cs deleted file mode 100644 index f24f04e..0000000 --- a/Server/Exceptions/LogIn/UserNotFoundException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Server.Exceptions.LogIn -{ - public class UserNotFoundException : LoginErrorException - { - public UserNotFoundException() - { - } - public UserNotFoundException(string message) : - base($"This user is not found!") - { - } - } -} diff --git a/Server/Exceptions/Register/AlreadyExistsException.cs b/Server/Exceptions/Register/AlreadyExistsException.cs deleted file mode 100644 index 0861364..0000000 --- a/Server/Exceptions/Register/AlreadyExistsException.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Server.Exceptions.Register -{ - public class AlreadyExistsException : UnknownReasonException - { - public AlreadyExistsException() - { - - } - - public AlreadyExistsException(string message) : base(string.Format($"This account already exists")) - { - - } - } -} \ No newline at end of file diff --git a/Server/Exceptions/Register/UnknownReasonException.cs b/Server/Exceptions/Register/UnknownReasonException.cs deleted file mode 100644 index 1fc2609..0000000 --- a/Server/Exceptions/Register/UnknownReasonException.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Server.Exceptions.Register -{ - public class UnknownReasonException : Exception - { - public UnknownReasonException() - { - - } - - public UnknownReasonException(string message) : - base(message) - { - - } - - public UnknownReasonException(string message, Exception inner) : - base(message, inner) - { - - } - } -} \ No newline at end of file diff --git a/Server/GameLogic/Exceptions/TwinkGameRoomCreationException.cs b/Server/GameLogic/Exceptions/TwinkGameRoomCreationException.cs deleted file mode 100644 index 180040a..0000000 --- a/Server/GameLogic/Exceptions/TwinkGameRoomCreationException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Server.GameLogic.Exceptions -{ - public class TwinkGameRoomCreationException:Exception - { - public TwinkGameRoomCreationException() : - base($"Failed to creaate one more game when you are sitting in another room!") - { - } - } -} diff --git a/Server/GameLogic/LogicServices/IRoomCoordinator.cs b/Server/GameLogic/LogicServices/IRoomCoordinator.cs deleted file mode 100644 index 2517a43..0000000 --- a/Server/GameLogic/LogicServices/IRoomCoordinator.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Server.GameLogic.Models.Impl; -using System.Collections.Concurrent; -using System.Threading.Tasks; - -namespace Server.GameLogic.LogicServices -{ - public interface IRoomCoordinator - { - - /// - /// List of active rooms. Made for monitoring - /// - ConcurrentDictionary ActiveRooms { get; } - - /// - /// Method to create a room - /// - /// - /// - /// - Task CreateRoom(string sessionId, bool isPrivate); - - /// - /// Method to join a private room by id - /// - /// - /// - /// - Task JoinPrivateRoom(string sessionId, string roomId); - - /// - /// Method to join random public room - /// - /// - /// - Task JoinPublicRoom(string sessionId); - - /// - /// Method to play with Bot - /// - /// - /// - Task CreateTrainingRoom(string sessionId); - - /// - /// OBSOLETE method to update Rooms - /// - /// - /// - Task UpdateRoom(Room updated); - - /// - /// Method to update Rooms - /// - /// - /// - Task UpdateRoom(string roomId); - - /// - /// Updates player status - /// - /// - /// - /// - Task UpdatePlayerStatus(string sessionId, bool isReady); - - /// - /// Deletes room - /// - /// - /// - Task DeleteRoom(string roomId); - } -} diff --git a/Server/GameLogic/LogicServices/IRoundCoordinator.cs b/Server/GameLogic/LogicServices/IRoundCoordinator.cs deleted file mode 100644 index bb9f9f3..0000000 --- a/Server/GameLogic/LogicServices/IRoundCoordinator.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Concurrent; -using System.Threading.Tasks; -using Server.GameLogic.Models.Impl; - -namespace Server.GameLogic.LogicServices -{ - public interface IRoundCoordinator - { - /// - /// Task to make a move - /// - /// current room Id - /// Current account Id - /// Move that he has done - /// Round - Task MakeMove(string roomId, string accountId, int move); - - /// - /// Dictionary of active rounds. Made for monitoring - /// - ConcurrentDictionary ActiveRounds { get; set; } - - /// - /// Gets current active round for special room - /// - /// id of this round - /// Round - Task GetCurrentActiveRoundForSpecialRoom(string roundId); - - /// - /// Updates rounds - /// - /// id of this room - /// Round - Task UpdateRound(string roomId); - } -} \ No newline at end of file diff --git a/Server/GameLogic/LogicServices/Impl/RoomCoordinator.cs b/Server/GameLogic/LogicServices/Impl/RoomCoordinator.cs deleted file mode 100644 index 0f8bdb5..0000000 --- a/Server/GameLogic/LogicServices/Impl/RoomCoordinator.cs +++ /dev/null @@ -1,296 +0,0 @@ -using Server.Exceptions.LogIn; -using Server.GameLogic.Exceptions; -using Server.GameLogic.Models.Impl; -using Server.Models; -using Server.Services.Interfaces; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Server.GameLogic.Models; - -namespace Server.GameLogic.LogicServices.Impl -{ - public class RoomCoordinator : IRoomCoordinator - { - private readonly IAccountManager _accountManager; - - private readonly IRoundCoordinator _roundCoordinator; - - private static readonly Random Random = new(); - - private Timer _timer; - - public ConcurrentDictionary ActiveRooms { get; } - - public RoomCoordinator( - IAccountManager accountManager, - IRoundCoordinator roundCoordinator) - { - _accountManager = accountManager; - _roundCoordinator = roundCoordinator; - ActiveRooms = new ConcurrentDictionary(); - _timer = new Timer(CheckRoomDate, null, 0, 10000); - } - - public async Task CreateRoom(string sessionId, bool isPrivate) - { - var tasks = Task.Factory.StartNew(() => - { - var account = GetAccountBySessionId(sessionId); - - if (ActiveRooms.Any(x => x.Value.Players.Any(p => p.Key.Equals(account.Id)))) - throw new TwinkGameRoomCreationException(); - - var newRoom = new Room - { - RoomId = RandomString(), - Players = new ConcurrentDictionary(), - IsPrivate = isPrivate, - IsReady = false, - IsRoundEnded = false, - IsFull = false, - CreationTime = DateTime.Now - }; - - if (newRoom.Players.TryAdd(account.Id, false)) - { - ActiveRooms.TryAdd(newRoom.RoomId, newRoom); - } - - //_timer = new Timer(tm, null, 0, 10000); //todo: implement - return newRoom; - }); - return await tasks; - } - - public async Task JoinPublicRoom(string sessionId) - { - var tasks = Task.Factory.StartNew(() => - { - var thisRoom = ActiveRooms - .FirstOrDefault(x => - x.Value.IsPrivate == false - && x.Value.Players.Count < 2) - .Value; - - var thisAccount = GetAccountBySessionId(sessionId); - - thisRoom.Players.TryAdd(thisAccount.Id, false); - - return UpdateRoom(thisRoom).Result; - }); - return await tasks; - } - - public async Task CreateTrainingRoom(string sessionId) - { - var tasks = Task.Factory.StartNew(() => - { - var account = GetAccountBySessionId(sessionId); - - if (ActiveRooms.Any(x => x.Value.Players.Any(p => p.Key.Equals(account.Id)))) - throw new TwinkGameRoomCreationException(); - - var newRoom = new Room - { - RoomId = RandomString(), - Players = new ConcurrentDictionary(), - IsPrivate = true, - IsReady = false, - IsRoundEnded = false, - IsFull = false, - CreationTime = DateTime.Now - }; - - newRoom.Players.TryAdd(account.Id, false); - newRoom.Players.TryAdd("Bot", true); - newRoom.IsFull = true; - ActiveRooms.TryAdd(newRoom.RoomId, newRoom); - - return newRoom; - }); - return await tasks; - } - - public async Task JoinPrivateRoom(string sessionId, string roomId) - { - var tasks = Task.Run(() => - { - if (!ActiveRooms.TryGetValue(roomId, out var thisRoom)) - return null; //todo:exception; - - if (thisRoom.Players.Count == 2) - return null; - - var newRoom = thisRoom; - var thisAccount = GetAccountBySessionId(sessionId); - newRoom.Players.TryAdd(thisAccount.Id, false); - - if (newRoom.Players.Count > 1) - newRoom.IsFull = true; - - return ActiveRooms.TryUpdate(roomId, - newRoom, thisRoom) - ? newRoom - : null; //todo: change to exception; - }); - - return await tasks; - } - - private async void CheckRoomDate(object state) - { - var threads = Task.Factory.StartNew(() => - { - if (ActiveRooms.IsEmpty) return; - foreach (var room in ActiveRooms) - { - if (room.Value.CreationTime.AddMinutes(5) < DateTime.Now && room.Value.CurrentRoundId == null) - ActiveRooms.TryRemove(room); - } - }); - await Task.WhenAll(threads); - } - - public async Task DeleteRoom(string roomId) - { - var tasks = Task.Factory.StartNew(() => - ActiveRooms.TryRemove(roomId, out _)); - return await tasks; - } - - public async Task UpdateRoom(Room updated) - { - var thread = Task.Factory.StartNew(() => - { - ActiveRooms.TryGetValue(updated.RoomId, out var room); - if (room == null) - { - return null; //todo: change into exception; - } - - return ActiveRooms.TryUpdate(room.RoomId, - updated, room) - ? room - : null; - }); - return await thread; - } - public async Task UpdatePlayerStatus(string sessionId, bool isReady) - { - var account = GetAccountBySessionId(sessionId); - - var room = ActiveRooms.Values - .FirstOrDefault(x => x.Players.Keys - .Any(p => p - .Equals(account.Id))); - - var thisRoom = GetRoomByRoomId(room?.RoomId); - - if (thisRoom == null) - return null; //Never performs - - - var (key, oldValue) = - thisRoom.Players.FirstOrDefault(x => x.Key == account.Id); - - - thisRoom.Players.TryUpdate(key, isReady, oldValue); - - if (thisRoom.Players.Values.All(x => x) && thisRoom.Players.Count == 2) - { - thisRoom.IsReady = true; - - thisRoom.IsFull = true; - - if (thisRoom.CurrentRoundId != null) - return thisRoom; - - var round = new Round - { - Id = Guid.NewGuid() - .ToString(), - IsFinished = false, - PlayerMoves = new ConcurrentDictionary(), - TimeFinished = DateTime.Now, - WinnerId = null, - LoserId = null, - }; - - foreach (var value in thisRoom.Players.Keys.ToList()) - { - round.PlayerMoves.TryAdd(value, RequiredGameMove.Default); - } - - thisRoom.CurrentRoundId = round.Id; - - _roundCoordinator.ActiveRounds.TryAdd(thisRoom.RoomId, round); - - } - - return await UpdateRoom(thisRoom); - } - public async Task UpdateRoom(string roomId) - { - try - { - var room = GetRoomByRoomId(roomId); - - var thisRound = _roundCoordinator.ActiveRounds.FirstOrDefault(x => x.Key.Equals(room.RoomId)); - - if (thisRound.Value != null && thisRound.Value.IsFinished) - { - room.IsReady = false; - room.IsRoundEnded = false; - room.CurrentRoundId = null; - foreach (var (key, value) in room.Players) - { - if (key.Equals("Bot")) - room.Players.TryUpdate(key, true, value); - else - { - room.Players.TryUpdate(key, false, value); - } - } - _roundCoordinator.ActiveRounds.TryRemove(thisRound); - - await UpdateRoom(room); - } - - return await UpdateRoom(room); - } - catch (Exception exception) - { - return null; - } - - } - - #region PrivateMethods - private static string RandomString() - { - const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, 5) - .Select(s => s[Random.Next(s.Length)]).ToArray()); - } - private Account GetAccountBySessionId(string sessionId) - { - _accountManager.AccountsActive.TryGetValue(sessionId, out var account); - if (account != null) - return account; - throw new UserNotFoundException(nameof(account)); - - } - private Room GetRoomByRoomId(string roomId) - { - return ActiveRooms.TryGetValue(roomId, out var thisRoom) - ? thisRoom - : throw new UserNotFoundException(); - } - - #endregion - - } -} \ No newline at end of file diff --git a/Server/GameLogic/LogicServices/Impl/RoundCoordinator.cs b/Server/GameLogic/LogicServices/Impl/RoundCoordinator.cs deleted file mode 100644 index 81ea48e..0000000 --- a/Server/GameLogic/LogicServices/Impl/RoundCoordinator.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; -using Server.GameLogic.Models; -using Server.GameLogic.Models.Impl; -using Server.Models; -using Server.Services.Interfaces; - -namespace Server.GameLogic.LogicServices.Impl -{ - public class RoundCoordinator : IRoundCoordinator - { - private readonly IStorage _storageRounds; - - private readonly IStorage _storageStatistics; - - private readonly IAccountManager _accountManager; - - public ConcurrentDictionary ActiveRounds { get; set; } - - public RoundCoordinator( - IStorage storageRounds, - IAccountManager accountManager, - IStorage storageStatistics) - { - _storageRounds = storageRounds; - _accountManager = accountManager; - _storageStatistics = storageStatistics; - ActiveRounds = new ConcurrentDictionary(); - } - - - public async Task MakeMove(string roomId, string sessionId, int move) - { - var tasks = Task.Factory.StartNew(async () => - { - var accountId = _accountManager.GetActiveAccountBySessionId(sessionId).Id; - ActiveRounds.TryGetValue(roomId, out var thisRound); - - if (thisRound == null) - return null; //todo: exception; - - if (thisRound.IsFinished) - { - return thisRound; - } - - //************************************************************************************************************************************ - var elapsedTime = DateTime.Now.Subtract(thisRound.TimeFinished); - if (elapsedTime.Seconds>= 200 && - thisRound.PlayerMoves.Any(x => x.Value.Equals(RequiredGameMove.Default))) - { - var dictionary = thisRound.PlayerMoves; - var first = dictionary.Keys.First(); - var last = dictionary.Keys.Last(); - - if (dictionary[first] == dictionary[last]) - thisRound.IsDraw = false; - else if (dictionary[first] == RequiredGameMove.Default) - { - thisRound.LoserId = first; - thisRound.WinnerId = last; - } - else - { - thisRound.LoserId = last; - thisRound.WinnerId = first; - } - thisRound.TimeFinished = DateTime.Now; - thisRound.IsFinished = true; - - await UpdateRound(thisRound); - return thisRound; - } - - thisRound.TimeFinished = DateTime.Now; - - //************************************************************************************************************************ - - var botPlays = false; - if (thisRound.PlayerMoves.Any(x => x.Key.Equals("Bot"))) - { - thisRound.PlayerMoves = RockPaperScissors.ChangeBotState(thisRound.PlayerMoves); - botPlays = true; - } - thisRound.PlayerMoves = RockPaperScissors.UpdateMove(thisRound.PlayerMoves, accountId, move); - - if (thisRound.PlayerMoves.Values.All(x => x != RequiredGameMove.Default)) - { - var winner = RockPaperScissors.MoveComparator(thisRound.PlayerMoves); - - if (string.IsNullOrEmpty(winner)) - { - thisRound.IsDraw = false; - thisRound.WinnerId = "DRAW"; - thisRound.LoserId = "DRAW"; - await UpdateRound(thisRound); - } - - if (botPlays) - { - thisRound.IsFinished = true; - var thisPlayerKey = - thisRound.LoserId = thisRound.PlayerMoves - .FirstOrDefault(x => x.Key != "Bot").Key; - if (winner == "Bot") - { - thisRound.WinnerId = winner; - thisRound.LoserId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==thisPlayerKey).Value.Login; - } - else - { - thisRound.WinnerId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==winner).Value.Login; - thisRound.LoserId = "Bot"; - } - } - - else - { - var loserId = thisRound.PlayerMoves.FirstOrDefault(x => x.Key != winner).Key; - - //////////////////////////////////////////////////////////////////////////// - if (thisRound.WinnerId == "DRAW" || thisRound.LoserId == "DRAW") - { - thisRound.IsFinished = true; - thisRound.WinnerId = "DRAW"; - thisRound.LoserId = "DRAW"; - thisRound.TimeFinished = DateTime.Now; - } - else - { - thisRound.IsFinished = true; - thisRound.WinnerId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==winner).Value.Login; - thisRound.LoserId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==loserId).Value.Login; - thisRound.TimeFinished = DateTime.Now; - } - _storageRounds.Add(thisRound); - await FillStatistics(thisRound); - } - - } - - await UpdateRound(thisRound); - - return thisRound; - - }); - return await await tasks; //AWAIT AWAIT? - - } - - private async Task FillStatistics(IRound thisRound) - { - if (thisRound.WinnerId == "DRAW" || thisRound.LoserId == "DRAW") - return; - var keys = thisRound.PlayerMoves.Keys; - foreach (var key in keys) //FIX - { - var thisAccountLogin = _accountManager.AccountsActive.FirstOrDefault(x => x.Value.Id == key); - var statistics = await _storageStatistics.GetAsync(key); //here is the problem. - - if (thisRound.WinnerId.Equals(thisAccountLogin.Value.Login)) - { - statistics.Wins += 1; - statistics.Score += 4; - } - else - { - statistics.Loss += 1; - - if (statistics.Score == 0) - statistics.Score = 0; - else - { - statistics.Score -= 2; - } - } - - var playerMove = - thisRound.PlayerMoves.FirstOrDefault(x => x.Key.Equals(key)).Value; - switch (playerMove) //NOT TO ADD ANYTHING ELSE - { - case RequiredGameMove.Paper: - statistics.UsedPaper += 1; - break; - case RequiredGameMove.Rock: - statistics.UsedRock += 1; - break; - case RequiredGameMove.Scissors: - statistics.UsedScissors += 1; - break; - } - - if (statistics.Loss != 0) - // ReSharper disable once PossibleLossOfFraction - statistics.WinLossRatio = statistics.Wins / statistics.Loss * 100d; - else - { - statistics.WinLossRatio = 100d; - } - - var allRound = await _storageRounds.GetAllAsync(); - - int wins=0, loss=0; - - foreach (var round in allRound) - { - if (!InRange(round.TimeFinished,DateTime.Now.AddDays(-7), DateTime.Now)) continue; - if (round.WinnerId.Equals(key)) - wins++; - else if (round.LoserId.Equals(key)) - { - loss++; - } - } - - var winRate = 0d; - if (loss == 0) - winRate = 0d; - else - { - winRate = (float) wins / loss * 100d; - } - - statistics.TimeSpent = $"Last 7 days win rate: {winRate}%"; - - await _storageStatistics.UpdateAsync(key, statistics); - - } - } - - private static bool InRange(DateTime dateToCheck, DateTime startDate, DateTime endDate) - { - return dateToCheck >= startDate && dateToCheck < endDate; - } - - - //***************************** - private async Task UpdateRound(Round updated) - { - var task = Task.Factory.StartNew(async () => - { - var roomId = - ActiveRounds.Where(x => x.Value - .Equals(updated)).ToArray(); - - - - if (updated.IsFinished) - { - //if (!updated.PlayerMoves.All(x => x.Key != "Bot") || updated.IsDraw) return updated; - - - //ActiveRounds.TryRemove(roomId[0].Key, out _); - - return updated; - } - ActiveRounds.TryGetValue(roomId[0].Key, out var oldRoom); - ActiveRounds.TryUpdate(roomId[0].Key, updated, oldRoom); - - return updated; - }); - - return await await task; - } - - //******************************** - public async Task UpdateRound(string roomId) - { - var task = Task.Factory.StartNew(async () => - { - ActiveRounds.TryGetValue(roomId, out var updated); - - if (updated == null) - return null; //todo: add exception; - - //*************************************************************** - var elapsedTime = DateTime.Now.Subtract(updated.TimeFinished); - if (elapsedTime.Seconds>= 20 && - updated.PlayerMoves.Any(x => x.Value.Equals(RequiredGameMove.Default))) - { - var dictionary = updated.PlayerMoves; - var first = dictionary.Keys.First(); - var last = dictionary.Keys.First(); - - if (dictionary[first] == dictionary[last]) - updated.IsDraw = false; - else if (dictionary[first] == RequiredGameMove.Default) - { - updated.LoserId = first; - updated.WinnerId = last; - } - else - { - updated.LoserId = last; - updated.WinnerId = first; - } - updated.TimeFinished = DateTime.Now; - updated.IsFinished = true; - - await UpdateRound(updated); - return updated; - } - //***************************************************************** - - if (updated.IsFinished) - { - //if(updated.PlayerMoves.Keys.Any(x=> x!="Bot")) - //await _storageRounds.AddAsync(updated); - - //ActiveRounds.TryRemove(roomId, out _); - - return updated; - } - - ActiveRounds.TryGetValue(roomId, out var oldRoom); //Do something with this - ActiveRounds.TryUpdate(roomId, updated, oldRoom); - - return updated; - }); - - return await await task; //Task>?????????????????? - } - public async Task GetCurrentActiveRoundForSpecialRoom(string roundId) - { - var tasks = Task.Factory.StartNew(() => ActiveRounds.TryGetValue(roundId, out var thisRound) ? thisRound : null); - //todo: change null to exception; - return await tasks; - } - } -} \ No newline at end of file diff --git a/Server/GameLogic/Models/IRoom.cs b/Server/GameLogic/Models/IRoom.cs deleted file mode 100644 index de5ec72..0000000 --- a/Server/GameLogic/Models/IRoom.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; - -namespace Server.GameLogic.Models -{ - public interface IRoom - { - [JsonProperty("RoomId")] - string RoomId { get; set; } - [JsonProperty("Players")] - ConcurrentDictionary Players { get; set; } - [JsonProperty("CurrentRoundId")] - string CurrentRoundId { get; set; } - [JsonProperty("CreationTime")] - DateTime CreationTime { get; set; } - } - -} diff --git a/Server/GameLogic/Models/IRound.cs b/Server/GameLogic/Models/IRound.cs deleted file mode 100644 index 96b9509..0000000 --- a/Server/GameLogic/Models/IRound.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Text.Json.Serialization; - -namespace Server.GameLogic.Models -{ - public interface IRound - { - [JsonPropertyName("Id")] - string Id { get; init; } - - [JsonPropertyName("RoomId")] - string RoomId { get; set; } - - [JsonPropertyName("Moves")] - public ConcurrentDictionary PlayerMoves { get; set; } - - [JsonPropertyName("IsFinished")] - bool IsFinished { get; set; } - - [JsonPropertyName("TimeFinished")] - DateTime TimeFinished { get; set; } - - [JsonPropertyName("WinnerId")] - string WinnerId { get; set; } - - [JsonPropertyName("LoserId")] - string LoserId { get; set; } - public bool IsDraw { get; set; } - } -} diff --git a/Server/GameLogic/Models/Impl/Room.cs b/Server/GameLogic/Models/Impl/Room.cs deleted file mode 100644 index f97de69..0000000 --- a/Server/GameLogic/Models/Impl/Room.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Concurrent; - -namespace Server.GameLogic.Models.Impl -{ - public class Room : IRoom - { - /// - /// Id of the room. Consists of 5 randomized chars - /// - public string RoomId { get; set; } - - /// - /// ConcurrentDictionary of all players. - /// Where string - account Id. Bool - flag is he ready - /// - public ConcurrentDictionary Players { get; set; } - - /// - /// Id of current round - /// - public string CurrentRoundId { get; set; } - - /// - /// Flag is this room is private - /// - public bool IsPrivate { get; set; } - - /// - /// Flag if everyone in this room is ready - /// - public bool IsReady { get; set; } - - /// - /// Flag if room is full - /// - public bool IsFull { get; set; } - - /// - /// Creation date. After 5 minutes of inactivity will be deleted - /// - public DateTime CreationTime { get; set; } - - /// - /// Flag is current count has ended - /// - public bool IsRoundEnded { get; set; } - - } -} diff --git a/Server/GameLogic/Models/Impl/Round.cs b/Server/GameLogic/Models/Impl/Round.cs deleted file mode 100644 index 40051be..0000000 --- a/Server/GameLogic/Models/Impl/Round.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Concurrent; - -namespace Server.GameLogic.Models.Impl -{ - public class Round : IRound - { - /// - /// Id of current round. GUID - /// - public string Id { get; init; } - - /// - /// Id of room, Where this round is situated. OBSOLETE - /// - - public string RoomId { get; set; } - - /// - /// Flag is this round has ended - /// - public bool IsFinished { get; set; } - - /// - /// Dictionary of player Id and his move - /// - public ConcurrentDictionary PlayerMoves { get; set; } - - /// - /// Is the result is draw - /// - public bool IsDraw { get; set; } - - /// - /// Time of finishing this room. Also used to check 20 seconds for the move. - /// - public DateTime TimeFinished { get; set; } - - /// - /// Login of winner Id - /// - public string WinnerId { get; set; } - - /// - /// Login of loser Id - /// - public string LoserId { get; set; } - - } -} diff --git a/Server/GameLogic/Models/RequiredGameMove.cs b/Server/GameLogic/Models/RequiredGameMove.cs deleted file mode 100644 index bcf6cd4..0000000 --- a/Server/GameLogic/Models/RequiredGameMove.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Server.GameLogic.Models -{ - /// - /// Enumerator of available player operations - /// - public enum RequiredGameMove - { - Default = 0, - Rock = 1, - Paper = 2, - Scissors = 3 - } -} diff --git a/Server/GameLogic/RockPaperScissors.cs b/Server/GameLogic/RockPaperScissors.cs deleted file mode 100644 index acd94ce..0000000 --- a/Server/GameLogic/RockPaperScissors.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using Server.GameLogic.Models; - -namespace Server.GameLogic -{ - public static class RockPaperScissors - { - public static ConcurrentDictionary UpdateMove( - ConcurrentDictionary playerMoves, string accountId, int move) - { - playerMoves.TryUpdate(accountId, (RequiredGameMove)move, - playerMoves.FirstOrDefault(x=> x.Key.Equals(accountId)).Value); - - return playerMoves; - } - - /// - /// Implementation or Rock Paper Scissors Game. - /// - /// ConcurrentDictionary of players - /// string Winner - public static string MoveComparator( - ConcurrentDictionary playerMoves) - { - var surrenderMove = 0; - var winner = string.Empty; - - foreach (var (key, value) in playerMoves) - { - - if (surrenderMove == 0 && string.IsNullOrEmpty(winner)) - { - winner = key; - surrenderMove = (int)value; - } - else - { - if ((int)value == surrenderMove) - { - return string.Empty; - } - - if (surrenderMove == 1 && (int)value == 3) - continue; - if (surrenderMove == 2 && (int)value == 1) - continue; - else if (surrenderMove == 3 && (int)value == 2) - continue; - else if (surrenderMove == 1 && (int)value == 2) - winner = key; - else if (surrenderMove == 2 && (int)value == 3) - winner = key; - else if (surrenderMove == 3 && (int)value == 1) - winner = key; - } - } - return winner; - } - - /// - /// Changes Bot move if he is in room - /// - /// ConcurrentDictionary of players - /// ConcurrentDictionary of players - public static ConcurrentDictionary ChangeBotState( - ConcurrentDictionary playerMoves) - { - var (key, value) = playerMoves.FirstOrDefault(x => x.Key.Equals("Bot")); - playerMoves.TryUpdate(key, GenerateRandomMove(), value); - - return playerMoves; - } - - /// - /// Gets randomized move for the bot - /// - /// Enumerator RequiredGameMove - private static RequiredGameMove GenerateRandomMove() - { - var r = new Random(); - var rInt = r.Next(1, 4); - - return (RequiredGameMove)rInt; - } - } - -} \ No newline at end of file diff --git a/Server/Mappings/StatisticsMappings.cs b/Server/Mappings/StatisticsMappings.cs deleted file mode 100644 index 82aa919..0000000 --- a/Server/Mappings/StatisticsMappings.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Server.Contracts; -using Server.Models; - -namespace Server.Mappings -{ - public static class StatisticsMappings - { - /// - /// Method to map Big statistics to a small overall statistics - /// - /// Big statistics of all accounts - /// StatisticsDto - public static StatisticsDto ToStatisticsDto(this Statistics statistics) - { - return statistics == null - ? null - : new StatisticsDto - { - Login = statistics.Login, - Score = statistics.Score - - }; - - } - } -} \ No newline at end of file diff --git a/Server/Models/Account.cs b/Server/Models/Account.cs deleted file mode 100644 index 2e13543..0000000 --- a/Server/Models/Account.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Server.Models.Interfaces; - -namespace Server.Models -{ - public class Account : IAccount - { - - /// - /// Id of account. Unique to everyone and similar with Statistics Id - /// - public string Id { get; init; } - - /// - /// Nick name of Account - /// - public string Login { get; set; } - - /// - /// Password of the Account - /// - [StringLength(20, MinimumLength=5, ErrorMessage = "Invalid password length")] - public string Password { get; set; } - - } -} \ No newline at end of file diff --git a/Server/Models/Interfaces/IAccount.cs b/Server/Models/Interfaces/IAccount.cs deleted file mode 100644 index dd2d657..0000000 --- a/Server/Models/Interfaces/IAccount.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; -namespace Server.Models.Interfaces -{ - public interface IAccount - { - [JsonPropertyName("Id")] - string Id { get; } - - [JsonPropertyName("Login")] - string Login { get; set; } - - [JsonPropertyName("Password")] - string Password { get; set; } - - - - } -} \ No newline at end of file diff --git a/Server/Models/Interfaces/IStatistics.cs b/Server/Models/Interfaces/IStatistics.cs deleted file mode 100644 index f6d05c7..0000000 --- a/Server/Models/Interfaces/IStatistics.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Server.Models.Interfaces -{ - public interface IStatistics - { - [JsonPropertyName("Id")] - string Id { get; set; } - - [JsonPropertyName("UserLogin")] - string Login { get; set; } - - [JsonPropertyName("Wins")] - int Wins { get; set; } - - [JsonPropertyName("Loss")] - int Loss { get; set; } - - [JsonPropertyName("WinToLossRatio")] - double WinLossRatio { get; set; } - - [JsonPropertyName("TimeSpent")] - string TimeSpent { get; set; } - - [JsonPropertyName("UsedRock")] - int UsedRock { get; set; } - - [JsonPropertyName("UsedPaper")] - int UsedPaper { get; set; } - - [JsonPropertyName("UsedScissors")] - int UsedScissors { get; set; } - - [JsonPropertyName("Score")] - int Score { get; set; } - - } -} \ No newline at end of file diff --git a/Server/Models/Statistics.cs b/Server/Models/Statistics.cs deleted file mode 100644 index 526bcee..0000000 --- a/Server/Models/Statistics.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Server.Models.Interfaces; - -namespace Server.Models -{ - public class Statistics : IStatistics - { - /// - /// Id of statistics. Equivalent to Account Id - /// - public string Id { get; set; } - - /// - /// Nick name of Account - /// - public string Login { get; set; } - - /// - /// Total amount of Wins - /// - public int Wins { get; set; } - - /// - /// Total amount of Loses - /// - public int Loss { get; set; } - - /// - /// Total amount of Draws. OBSOLETE - /// - - public int Draws { get; set; } - - /// - /// Ratio Wins to Losses. Win/Loss * 100 - /// - public double WinLossRatio { get; set; } - - /// - /// Ratio for the last 7 days - /// - public string TimeSpent { get; set; } - - /// - /// Times used rock - /// - public int UsedRock { get; set; } - - /// - /// Times used Paper - /// - public int UsedPaper { get; set; } - - /// - /// Times used Scissors - /// - public int UsedScissors { get; set; } - - /// - /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points. - /// - public int Score { get; set; } - - - - } -} \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs deleted file mode 100644 index bb16d90..0000000 --- a/Server/Program.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Serilog; - -namespace Server -{ - public static class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureLogging(loggingBuilder => - { - loggingBuilder.ClearProviders(); - loggingBuilder.SetMinimumLevel(LogLevel.Information); - loggingBuilder.AddSerilog(new LoggerConfiguration() - .WriteTo.Console() - .WriteTo.File("app.log") - .CreateLogger()); - }) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); - } -} \ No newline at end of file diff --git a/Server/Round b/Server/Round deleted file mode 100644 index e69de29..0000000 diff --git a/Server/Server.Authentication/AuthOptions.cs b/Server/Server.Authentication/AuthOptions.cs new file mode 100644 index 0000000..eb59595 --- /dev/null +++ b/Server/Server.Authentication/AuthOptions.cs @@ -0,0 +1,46 @@ +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Server.Authentication; + +/// +/// Options for JWT token.. +/// +public sealed class AuthOptions +{ + private static readonly SymmetricSecurityKey DefaultKey = new(Encoding.ASCII.GetBytes(PrivateKey)); + + /// + /// Token issuer (producer). + /// + public string Issuer { get; init; } = AppDomain.CurrentDomain.FriendlyName; + + /// + /// Token audience (consumer). + /// + public string Audience { get; init; } = "Player"; + + /// + /// Token secret part. + /// + public static string PrivateKey => "RockPaperScissors"; + + /// + /// Token life time. + /// + public TimeSpan LifeTime { get; init; } = TimeSpan.FromHours(3); + + /// + /// Require HTTPS. + /// + public bool RequireHttps { get; init; } = false; + + /// + /// Getting a symmetric security key. + /// + /// . + public static SymmetricSecurityKey GetSymmetricSecurityKey() + { + return DefaultKey; + } +} \ No newline at end of file diff --git a/Server/Server.Authentication/Exceptions/UserException.cs b/Server/Server.Authentication/Exceptions/UserException.cs new file mode 100644 index 0000000..d9cf49a --- /dev/null +++ b/Server/Server.Authentication/Exceptions/UserException.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Http; + +namespace Server.Authentication.Exceptions; + +/// +/// User-related custom structure exception. +/// +public sealed class UserException +{ + /// + /// Constructor. + /// + /// Exception message. + public UserException(string message) + { + Code = StatusCodes.Status400BadRequest; + Message = message; + } + + /// + /// Gets response code. + /// + public int Code { get; } + + /// + /// Gets exception message. + /// + public string Message { get; } +} \ No newline at end of file diff --git a/Server/Server.Authentication/Exceptions/UserExceptionsTemplates.cs b/Server/Server.Authentication/Exceptions/UserExceptionsTemplates.cs new file mode 100644 index 0000000..9695652 --- /dev/null +++ b/Server/Server.Authentication/Exceptions/UserExceptionsTemplates.cs @@ -0,0 +1,45 @@ +namespace Server.Authentication.Exceptions; + +/// +/// Compile-time exception templates for Authentication module. +/// +internal static class UserExceptionsTemplates +{ + /// + /// Builds exception template for Cooldown of Client login attempts. + /// + /// Client username. + /// Date of removing cooldown. + /// Created string message. + internal static string UserCoolDown(this string username, DateTimeOffset tillCooldownDate) + => $"User [{username}] has been cooled down till \"{tillCooldownDate}\"."; + + /// + /// Builds exception template for invalid credentials for user login attempt. + /// + /// Client input username. + /// Created string message. + internal static string UserInvalidCredentials(this string username) + => $"Invalid Credentials for user \"{username}\"."; + + /// + /// Builds exception template for not found client by input parameters. + /// + /// Client input username. + /// Created string message. + internal static string UserNotFound(this string username) + => $"User with login \"{username}\" is not found."; + + /// + /// Builds exception template for registering client, that already exists. + /// + /// Client input username. + /// Created string message. + internal static string UserAlreadyExists(this string username) + => $"User with login \"{username}\" already exists."; + + /// + /// Default template, when error is unknown. + /// + internal const string UnknownError = "Unknown error occured. Please try again."; +} \ No newline at end of file diff --git a/Server/Server.Authentication/Extensions/AuthenticationExtension.cs b/Server/Server.Authentication/Extensions/AuthenticationExtension.cs new file mode 100644 index 0000000..d6b76de --- /dev/null +++ b/Server/Server.Authentication/Extensions/AuthenticationExtension.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Server.Authentication.Services; + +namespace Server.Authentication.Extensions; + +/// +/// Authentication services extension. +/// +public static class AuthenticationExtension +{ + /// + /// Registers authentication. + /// + /// Service collection. + public static IServiceCollection AddAuthentications(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + services.AddOptions(); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + var jwtOptions = services + .BuildServiceProvider() + .GetRequiredService>() + .Value; + + options.RequireHttpsMetadata = jwtOptions.RequireHttps; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtOptions.Issuer, + + ValidateAudience = true, + ValidAudience = jwtOptions.Audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + + IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(), + ValidateIssuerSigningKey = true + }; + }); + + services.AddTransient(); + + return services; + } +} \ No newline at end of file diff --git a/Server/Server.Authentication/HashingBase64.cs b/Server/Server.Authentication/HashingBase64.cs new file mode 100644 index 0000000..6fe2eb0 --- /dev/null +++ b/Server/Server.Authentication/HashingBase64.cs @@ -0,0 +1,51 @@ +namespace Server.Authentication; + +/// +/// Default hashing base64 methods. +/// +public static class HashingBase64 +{ + /// + /// Decodes input string from BASE64 to a regular string. + /// + /// Encoded in BASE64 payload. + /// Decoded string. + public static string DecodeBase64(this string encodedData) + { + var encodedDataAsBytes + = Convert.FromBase64String(encodedData); + + return + System.Text.Encoding.ASCII.GetString(encodedDataAsBytes); + } + + /// + /// Encodes input string to BASE64 From a regular string. + /// + /// Payload to be encoded. + /// Encoded string. + public static string EncodeBase64(this string initialData) + { + var dataArray = System.Text.Encoding.ASCII.GetBytes(initialData); + + return Convert.ToBase64String(dataArray); + } + + /// + /// Compares initial encoded data with base64 data. + /// + /// Encoded data. + /// Initial payload. + /// + /// + /// true - when initial data, encoded in base64 is identical with base64 data. + /// + /// false - otherwise. + /// + public static bool IsHashEqual(this string base64Data, string initialData) + { + var baseDecoded = EncodeBase64(initialData); + + return base64Data.Equals(baseDecoded, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/Server/Server.Authentication/Models/AccountOutputModel.cs b/Server/Server.Authentication/Models/AccountOutputModel.cs new file mode 100644 index 0000000..7ad8fd2 --- /dev/null +++ b/Server/Server.Authentication/Models/AccountOutputModel.cs @@ -0,0 +1,17 @@ +namespace Server.Authentication.Models; + +/// +/// Model for output of user account. +/// +public sealed class AccountOutputModel +{ + /// + /// Gets or sets user token (used in header). + /// + public string Token { get; init; } + + /// + /// Gets or sets user login. + /// + public string Login { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Authentication/Models/Roles.cs b/Server/Server.Authentication/Models/Roles.cs new file mode 100644 index 0000000..76bcbb4 --- /dev/null +++ b/Server/Server.Authentication/Models/Roles.cs @@ -0,0 +1,17 @@ +namespace Server.Authentication.Models; + +/// +/// User roles. +/// +public static class Roles +{ + /// + /// Admin role. + /// + public const string Admin = "admin"; + + /// + /// User role. + /// + public const string User = "user"; +} \ No newline at end of file diff --git a/Server/Server.Authentication/Server.Authentication.csproj b/Server/Server.Authentication/Server.Authentication.csproj new file mode 100644 index 0000000..967b793 --- /dev/null +++ b/Server/Server.Authentication/Server.Authentication.csproj @@ -0,0 +1,19 @@ + + + + true + net6 + true + enable + + + + + + + + + + + + diff --git a/Server/Server.Authentication/Services/AttemptValidationService.cs b/Server/Server.Authentication/Services/AttemptValidationService.cs new file mode 100644 index 0000000..960d209 --- /dev/null +++ b/Server/Server.Authentication/Services/AttemptValidationService.cs @@ -0,0 +1,71 @@ +using System.Collections.Concurrent; + +namespace Server.Authentication.Services; + +/// +/// Static service with validation features. +/// +internal static class AttemptValidationService +{ + // key - userId. Value - count of failed attempts + private static readonly ConcurrentDictionary FailedAttempts = new(); + private static readonly ConcurrentDictionary CoolDownCollection = new(); + + public static bool TryInsertFailAttempt(this string userId) + { + if (string.IsNullOrEmpty(userId)) + { + return false; + } + + if (FailedAttempts.TryGetValue(userId, out var failedAttempts)) + { + if (failedAttempts >= 2) + { + // todo: in options + CoolDownCollection.TryAdd(userId, DateTimeOffset.UtcNow.AddMinutes(2)); + FailedAttempts.TryRemove(userId, out _); + + return true; + } + } + FailedAttempts.AddOrUpdate(userId, 1, (_, i) => i + 1); + + return true; + } + + public static int? CountFailedAttempts(this string userId) + { + if (string.IsNullOrEmpty(userId)) + { + return default; + } + + return FailedAttempts.TryGetValue(userId, out var failedAttempts) + ? failedAttempts + : default; + } + + public static bool IsCoolDown(this string userId, out DateTimeOffset coolDownDate) + { + if (string.IsNullOrEmpty(userId)) + { + coolDownDate = default; + return false; + } + + if (!CoolDownCollection.TryGetValue(userId, out coolDownDate)) + { + return false; + } + + if (coolDownDate >= DateTimeOffset.UtcNow) + { + return true; + } + + CoolDownCollection.TryRemove(userId, out coolDownDate); + + return false; + } +} \ No newline at end of file diff --git a/Server/Server.Authentication/Services/AuthService.cs b/Server/Server.Authentication/Services/AuthService.cs new file mode 100644 index 0000000..7edb77f --- /dev/null +++ b/Server/Server.Authentication/Services/AuthService.cs @@ -0,0 +1,185 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using OneOf; +using Server.Authentication.Exceptions; +using Server.Authentication.Models; +using Server.Data.Context; +using Server.Data.Entities; + +namespace Server.Authentication.Services; + +/// +internal sealed class AuthService : IAuthService +{ + private static readonly SigningCredentials SigningCredentials = new(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256); + private static readonly SemaphoreSlim Semaphore = new(initialCount: 1, maxCount: 1); + private static readonly JwtSecurityTokenHandler TokenHandler = new(); + + private readonly ServerContext _repository; + private readonly AuthOptions _authOptions; + private readonly ILogger _logger; + + /// + /// Constructor. + /// + /// . + /// . + /// . + public AuthService( + ILogger logger, + IOptions authOptions, + ServerContext repository) + { + _logger = logger; + _repository = repository; + _authOptions = authOptions.Value; + } + + /// + public async Task> RegisterAsync( + string login, + string password) + { + if (string.IsNullOrWhiteSpace(login)) + { + _logger.LogError("Login should not be 'empty'"); + + return new UserException(nameof(login).UserInvalidCredentials()); + } + + if (string.IsNullOrEmpty(password)) + { + _logger.LogError("Password should not be 'empty'"); + + return new UserException(nameof(password).UserInvalidCredentials()); + } + + var release = await Semaphore.WaitAsync(100); + + try + { + if (await _repository.Accounts.AnyAsync(account => account.Login.Equals(login.ToLower()))) + { + var exceptionMessage = login.UserAlreadyExists(); + + _logger.LogError("Error occured : {ExceptionMessage}", exceptionMessage); + + return new UserException(exceptionMessage); + } + + var accountId = Guid.NewGuid().ToString(); + + var account = new Account + { + Id = accountId, + Login = login, + Password = password.EncodeBase64(), + }; + + _repository.Accounts.Add(account); + + var accountStatistics = new Statistics + { + Id = accountId, + AccountId = accountId + }; + + _repository.StatisticsEnumerable.Add(accountStatistics); + await _repository.SaveChangesAsync(); + + return StatusCodes.Status200OK; + } + catch + { + _logger.LogWarning("Unable to process account for {Login}", login); + + return new UserException(UserExceptionsTemplates.UnknownError); + } + finally + { + if (release) + { + Semaphore.Release(); + } + } + } + + /// + public async Task> LoginAsync(string login, string password) + { + var userAccount = await _repository.Accounts.FirstOrDefaultAsync(account => account.Login.ToLower().Equals(login.ToLower())); + + string exceptionMessage; + + if (userAccount is null) + { + exceptionMessage = login.UserNotFound(); + _logger.LogWarning("Error occured: {ExceptionMessage}", exceptionMessage); + + return new UserException(exceptionMessage); + } + + if (login.IsCoolDown(out var coolRequestDate)) + { + exceptionMessage = login.UserCoolDown(coolRequestDate); + _logger.LogWarning("Error occured: {ExceptionMessage}", exceptionMessage); + + return new UserException(exceptionMessage); + } + + if (userAccount.Password.IsHashEqual(password)) + { + return new AccountOutputModel + { + Token = BuildToken(userAccount), + Login = userAccount.Login + }; + } + + login.TryInsertFailAttempt(); + + exceptionMessage = login.UserInvalidCredentials(); + _logger.LogWarning("Error occured: {ExceptionMessage}", exceptionMessage); + + return new UserException(exceptionMessage); + } + + private string BuildToken(Account accountModel) + { + var now = DateTime.UtcNow; + + var encodedJwt = TokenHandler.CreateEncodedJwt( + _authOptions.Issuer, + _authOptions.Audience, + GetClaimsIdentity(accountModel.Id), + now, + now.Add(_authOptions.LifeTime), + now, + SigningCredentials); + + return encodedJwt; + } + + private static ClaimsIdentity GetClaimsIdentity(string userId) + { + var claims = new[] + { + new Claim(ClaimsIdentity.DefaultNameClaimType, userId), + new Claim(ClaimsIdentity.DefaultRoleClaimType, Roles.User) + }; + var claimsIdentity = + new ClaimsIdentity( + claims, + JwtBearerDefaults.AuthenticationScheme, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + + return claimsIdentity; + } +} \ No newline at end of file diff --git a/Server/Server.Authentication/Services/IAuthService.cs b/Server/Server.Authentication/Services/IAuthService.cs new file mode 100644 index 0000000..b48a630 --- /dev/null +++ b/Server/Server.Authentication/Services/IAuthService.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using OneOf; +using Server.Authentication.Exceptions; +using Server.Authentication.Models; +using Server.Data.Entities; + +namespace Server.Authentication.Services; + +/// +/// Authentication service. +/// +public interface IAuthService +{ + /// + /// Registers new entity of Client. (). + /// + /// Client login, case insensitive. + /// Client password. + /// + /// + /// int - if everything is fine. + /// + /// UserException - If some case of error occured. (User exists, validation error, unknown error). + /// + Task> RegisterAsync(string login, string password); + + /// + /// Signs in client by credentials and building JWT token. + /// + /// Client login, case insensitive. + /// Client password. + /// + /// + /// AccountOutputModel - Constructed object with login and JWT token. + /// + /// UserException - If some case of error occured. (User exists, validation error, unknown error). + /// + Task> LoginAsync(string login, string password); +} \ No newline at end of file diff --git a/Server/Server.Bll/Exceptions/ExceptionTemplates.cs b/Server/Server.Bll/Exceptions/ExceptionTemplates.cs new file mode 100644 index 0000000..be194a1 --- /dev/null +++ b/Server/Server.Bll/Exceptions/ExceptionTemplates.cs @@ -0,0 +1,20 @@ +namespace Server.Bll.Exceptions; + +internal static class ExceptionTemplates +{ + // GENERAL EXCEPTION MESSAGES + internal const string Unknown = "Unknown error occured. Please try again"; + internal const string NotAllowed = "You are not allowed to do this."; + internal static string NotExists(string entity) => $"{entity} does not exist."; + + // ROOM EXCEPTIONS + internal const string TwinkRoom = "Failed to create one more game when you are sitting in another room."; + internal const string RoomFull = "This room is full."; + internal const string AlreadyInRoom = "You are already in room."; + internal const string RoomNotFull = "Room is not full."; + internal const string NoAvailableRooms = "Sorry, there are no public rooms available right now."; + + // ROUND EXCEPTIONS + internal const string RoundAlreadyCreated = "Round is already creaded."; + internal static string RoundNotFound(int roundId) => $"Round with id \"{roundId}\" is not found"; +} \ No newline at end of file diff --git a/Server/Server.Bll/Extensions/MappingExtensions.cs b/Server/Server.Bll/Extensions/MappingExtensions.cs new file mode 100644 index 0000000..e8c2e93 --- /dev/null +++ b/Server/Server.Bll/Extensions/MappingExtensions.cs @@ -0,0 +1,14 @@ +// using Mapster; +// using Server.Bll.Models; +// using Server.Data.Entities; +// +// namespace Server.Bll.Extensions; +// +// internal static class MappingExtensions +// { +// internal static readonly TypeAdapterSetter StatisticsAdapterConfig = +// TypeAdapterConfig +// .NewConfig() +// .Map(shortStatisticsModel => shortStatisticsModel.Login, statistics => statistics.Account.Login) +// .Map(shortStatisticsModel => shortStatisticsModel.Score, statistics => statistics.Score); +// } \ No newline at end of file diff --git a/Server/Server.Bll/Extensions/ServiceCollectionExtensions.cs b/Server/Server.Bll/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..5edff9d --- /dev/null +++ b/Server/Server.Bll/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +using Mapster; +using Microsoft.Extensions.DependencyInjection; +using Server.Bll.Models; +using Server.Bll.Services; +using Server.Bll.Services.Interfaces; +using Server.Data.Entities; + +namespace Server.Bll.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddBusinessLogic(this IServiceCollection service) + { + ArgumentNullException.ThrowIfNull(service); + + TypeAdapterConfig + .NewConfig() + .Map(shortStatisticsModel => shortStatisticsModel.Login, statistics => statistics.Account.Login) + .Map(shortStatisticsModel => shortStatisticsModel.Score, statistics => statistics.Score); + + service + .AddTransient() + .AddHostedService(); + + service.AddHttpContextAccessor(); + + service + .AddTransient() + .AddTransient(); + + return service; + } +} \ No newline at end of file diff --git a/Server/Server.Bll/Models/AccountModel.cs b/Server/Server.Bll/Models/AccountModel.cs new file mode 100644 index 0000000..d102163 --- /dev/null +++ b/Server/Server.Bll/Models/AccountModel.cs @@ -0,0 +1,6 @@ +namespace Server.Bll.Models; + +public sealed class AccountModel +{ + public string Login { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Bll/Models/PlayerModel.cs b/Server/Server.Bll/Models/PlayerModel.cs new file mode 100644 index 0000000..1586e6f --- /dev/null +++ b/Server/Server.Bll/Models/PlayerModel.cs @@ -0,0 +1,12 @@ +namespace Server.Bll.Models; + +public sealed class PlayerModel +{ + public string Id { get; init; } + + public int Move { get; init; } + + public bool IsReady { get; init; } + + public bool IsWinner { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Bll/Models/RoomModel.cs b/Server/Server.Bll/Models/RoomModel.cs new file mode 100644 index 0000000..b39b914 --- /dev/null +++ b/Server/Server.Bll/Models/RoomModel.cs @@ -0,0 +1,44 @@ +namespace Server.Bll.Models; + +public sealed class RoomModel +{ + /// + /// Id of the room. Consists of 5 randomized chars + /// + public string Id { get; init; } + + /// + /// Special code to join a room + /// + public string Code { get; init; } + + /// + /// Round, linked to this room + /// + public RoundModel? Round { get; init; } + + /// + /// . + /// + public ICollection? Players { get; init; } + + /// + /// Flag is this room is private + /// + public bool IsPrivate { get; init; } + + /// + /// Flag if room is full + /// + public bool IsFull { get; init; } + + /// + /// Creation date. After 5 minutes of inactivity will be deleted + /// + public long CreationTimeTicks { get; init; } + + /// + /// Last update time ticks. + /// + public long UpdateTicks { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Bll/Models/RoundModel.cs b/Server/Server.Bll/Models/RoundModel.cs new file mode 100644 index 0000000..043df70 --- /dev/null +++ b/Server/Server.Bll/Models/RoundModel.cs @@ -0,0 +1,14 @@ +namespace Server.Bll.Models; + +public sealed class RoundModel +{ + public string Id { get; init; } + + public bool IsFinished { get; init; } + + public long StartTimeTicks { get; init; } + + public long FinishTimeTicks { get; init; } + + public long UpdateTicks { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Bll/Models/ShortStatisticsModel.cs b/Server/Server.Bll/Models/ShortStatisticsModel.cs new file mode 100644 index 0000000..e349c94 --- /dev/null +++ b/Server/Server.Bll/Models/ShortStatisticsModel.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Server.Bll.Models; + +public sealed class ShortStatisticsModel +{ + [JsonPropertyName("login")] + public string Login { get; init; } + + /// + /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points. + /// + [JsonPropertyName("score")] + public int Score { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Bll/Models/StatisticsModel.cs b/Server/Server.Bll/Models/StatisticsModel.cs new file mode 100644 index 0000000..0ed68a3 --- /dev/null +++ b/Server/Server.Bll/Models/StatisticsModel.cs @@ -0,0 +1,50 @@ +namespace Server.Bll.Models; + +public sealed class StatisticsModel +{ + /// + /// Total amount of Wins + /// + public int? Wins { get; init; } + + /// + /// Total amount of Loses + /// + public int? Loss { get; init; } + + /// + /// Total amount of Draws. OBSOLETE + /// + + public int? Draws { get; init; } + + /// + /// Ratio Wins to Losses. Win/Loss * 100 + /// + public double? WinLossRatio { get; init; } + + /// + /// Ratio for the last 7 days + /// + public string TimeSpent { get; init; } + + /// + /// Times used rock + /// + public int? UsedRock { get; init; } + + /// + /// Times used Paper + /// + public int? UsedPaper { get; init; } + + /// + /// Times used Scissors + /// + public int? UsedScissors { get; init; } + + /// + /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points. + /// + public int? Score { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Bll/Options/CleanerOptions.cs b/Server/Server.Bll/Options/CleanerOptions.cs new file mode 100644 index 0000000..e43abfd --- /dev/null +++ b/Server/Server.Bll/Options/CleanerOptions.cs @@ -0,0 +1,12 @@ +namespace Server.Bll.Options; + +public sealed class CleanerOptions +{ + public const string Section = "Cleaning"; + + public TimeSpan CleanPeriod { get; init; } + + public TimeSpan RoomOutDateTime { get; init; } + + public TimeSpan RoundOutDateTime { get; init; } +} \ No newline at end of file diff --git a/Server/Server.Bll/Server.Bll.csproj b/Server/Server.Bll/Server.Bll.csproj new file mode 100644 index 0000000..4656682 --- /dev/null +++ b/Server/Server.Bll/Server.Bll.csproj @@ -0,0 +1,25 @@ + + + + true + true + net6 + true + enable + + + + + + + + + + + + + + + + + diff --git a/Server/Server.Bll/Services/CleanerBackgroundService.cs b/Server/Server.Bll/Services/CleanerBackgroundService.cs new file mode 100644 index 0000000..c8792d8 --- /dev/null +++ b/Server/Server.Bll/Services/CleanerBackgroundService.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Server.Bll.Options; +using Server.Bll.Services.Interfaces; + +namespace Server.Bll.Services; + +public sealed class CleanerBackgroundService : BackgroundService +{ + private readonly IServiceScopeFactory _serviceProvider; + private readonly ILogger _logger; + private readonly PeriodicTimer _periodicTimer; + private readonly CleanerOptions _cleanerOptions; + + public CleanerBackgroundService( + ILogger logger, + IServiceScopeFactory serviceProvider, + IOptions cleanerOptions) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _cleanerOptions = cleanerOptions?.Value ?? throw new ArgumentNullException(nameof(cleanerOptions)); + _periodicTimer = new PeriodicTimer(_cleanerOptions.CleanPeriod); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Starting Background service"); + + while (await _periodicTimer.WaitForNextTickAsync(stoppingToken)) + { + await CleanJunk(_serviceProvider); + } + } + + private async Task CleanJunk(IServiceScopeFactory factory) + { + using var scope = factory.CreateScope(); + var roomService = scope.ServiceProvider.GetRequiredService(); + var rooms = await roomService + .RemoveRangeAsync(_cleanerOptions.RoomOutDateTime, _cleanerOptions.RoundOutDateTime); + + if (rooms > 0) + { + _logger.LogInformation("Cleaned {Room} entities", rooms.ToString()); + } + } + + public override void Dispose() + { + _periodicTimer.Dispose(); + + base.Dispose(); + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + _periodicTimer.Dispose(); + + return base.StopAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/Server/Server.Bll/Services/Interfaces/IRoomService.cs b/Server/Server.Bll/Services/Interfaces/IRoomService.cs new file mode 100644 index 0000000..d3b0f46 --- /dev/null +++ b/Server/Server.Bll/Services/Interfaces/IRoomService.cs @@ -0,0 +1,22 @@ +using OneOf; +using RockPaperScissors.Common; +using Server.Bll.Models; + +namespace Server.Bll.Services.Interfaces; + +public interface IRoomService +{ + Task> CreateAsync(string userId, bool isPrivate = false, bool isTraining = false); + + Task RemoveRangeAsync(TimeSpan roomOutDate, TimeSpan roundOutDate); + + Task> JoinAsync(string userId, string? roomCode = null); + + Task> GetAsync(string roomId); + + Task GetUpdateTicksAsync(string roomId); + + Task> ChangePlayerStatusAsync(string userId, string roomId, bool newStatus); + + Task> DeleteAsync(string userId, string roomId); +} \ No newline at end of file diff --git a/Server/Server.Bll/Services/Interfaces/IRoundService.cs b/Server/Server.Bll/Services/Interfaces/IRoundService.cs new file mode 100644 index 0000000..baa9a19 --- /dev/null +++ b/Server/Server.Bll/Services/Interfaces/IRoundService.cs @@ -0,0 +1,12 @@ +using RockPaperScissors.Common; +using OneOf; +using RockPaperScissors.Common.Enums; + +namespace Server.Bll.Services.Interfaces; + +public interface IRoundService +{ + Task> MakeMoveAsync(string userId, string roundId, Move move); + + Task GetUpdateTicksAsync(string roundId); +} \ No newline at end of file diff --git a/Server/Server.Bll/Services/Interfaces/IStatisticsService.cs b/Server/Server.Bll/Services/Interfaces/IStatisticsService.cs new file mode 100644 index 0000000..92a06a3 --- /dev/null +++ b/Server/Server.Bll/Services/Interfaces/IStatisticsService.cs @@ -0,0 +1,12 @@ +using OneOf; +using RockPaperScissors.Common; +using Server.Bll.Models; + +namespace Server.Bll.Services.Interfaces; + +public interface IStatisticsService +{ + Task GetAllAsync(); + + Task> GetAsync(string userId); +} \ No newline at end of file diff --git a/Server/Server.Bll/Services/RoomService.cs b/Server/Server.Bll/Services/RoomService.cs new file mode 100644 index 0000000..012ffff --- /dev/null +++ b/Server/Server.Bll/Services/RoomService.cs @@ -0,0 +1,254 @@ +using Mapster; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using OneOf; +using RockPaperScissors.Common; +using Server.Bll.Exceptions; +using Server.Bll.Models; +using Server.Bll.Services.Interfaces; +using Server.Data.Context; +using Server.Data.Entities; +using Server.Data.Extensions; + +namespace Server.Bll.Services; + +internal sealed class RoomService : IRoomService +{ + private readonly ServerContext _repository; + + public RoomService(ServerContext repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public async Task> CreateAsync( + string userId, bool isPrivate, bool isTraining) + { + var doesRoomExist = await _repository.Rooms + .Include(room => room.Players) + .AnyAsync(roomPlayers => roomPlayers.Players.Any(player => player.AccountId == userId)); + + if (doesRoomExist) + { + return new CustomException(ExceptionTemplates.TwinkRoom); + } + + var players = new List(2) + { + new() + { + Id = Guid.NewGuid().ToString(), + Account = await _repository.Accounts.FindAsync(userId), + AccountId = userId, + IsReady = false, + } + }; + + var room = new Room + { + Id = Guid.NewGuid().ToString(), + IsPrivate = isPrivate, + Code = Guid.NewGuid().ToString("N")[..8], + IsFull = false, + CreationTimeTicks = DateTimeOffset.UtcNow.Ticks, + Players = players, + UpdateTicks = DateTimeOffset.UtcNow.Ticks + }; + + _repository.Rooms.Add(room); + + if (isTraining) + { + room.IsPrivate = true; + room.IsFull = true; + room.Players.Add(SeedingExtension.BotPlayer); + room.Round = RoundService.Create(room); + } + + await _repository.SaveChangesAsync(); + + return room.Adapt(); + } + + public async Task> ChangePlayerStatusAsync(string userId, string roomId, bool newStatus) + { + var room = await _repository.Rooms + .Include(room => room.Players) + .FirstOrDefaultAsync(room => room.Id == roomId); + + if (room is null) + { + return new CustomException($"Room with id '{roomId}' does not exist"); + } + + var currentPlayer = room.Players.FirstOrDefault(player => player.AccountId == userId); + if (currentPlayer is null) + { + return new CustomException("You are not able to modify this room"); + } + + currentPlayer.IsReady = newStatus; + + _repository.Players.Update(currentPlayer); + _repository.Rooms.Update(room); + + if (room.Players.All(player => player.IsReady)) + { + room.Round = RoundService.Create(room); + await _repository.SaveChangesAsync(); + } + + await _repository.SaveChangesAsync(); + + return room.Adapt(); + } + + public async Task GetUpdateTicksAsync(string roomId) + { + var room = await _repository.Rooms + .FindAsync(roomId); + + if (room is null) + { + return -1; + } + + return room.UpdateTicks; + } + + public async Task> JoinAsync(string userId, string? roomCode) + { + var oneOfRoom = string.IsNullOrEmpty(roomCode) ? await GetPublicAsync(userId) : await GetPrivateAsync(userId, roomCode); + + if (oneOfRoom.IsT1) + { + return oneOfRoom.AsT1; + } + + var room = oneOfRoom.AsT0; + + var newPlayer = new Player + { + Id = Guid.NewGuid().ToString(), + Account = await _repository.Accounts.FindAsync(userId), + AccountId = userId, + IsReady = false, + }; + + room.Players.Add(newPlayer); + room.UpdateTicks = DateTimeOffset.UtcNow.Ticks; + room.IsFull = room.Players.Count is 2; + + _repository.Rooms.Update(room); + + await _repository.SaveChangesAsync(); + + return room.Adapt(); + } + + public async Task> GetAsync(string roomId) + { + var room = await _repository.Rooms.FindAsync(roomId); + + if (room is null) + { + return new CustomException($"Room with id {roomId} does not exist"); + } + + return room.Adapt(); + } + + public async Task> DeleteAsync(string userId, string roomId) + { + var room = await _repository.Rooms.FindAsync(roomId); + + if (room is null) + { + return new CustomException($"Room with id {roomId} does not exist"); + } + + if (room.Players.All(player => player.AccountId != userId)) + { + return new CustomException("You have no rights to delete a room"); + } + + _repository.Rooms.Remove(room); + + await _repository.SaveChangesAsync(); + + return StatusCodes.Status200OK; + } + + public async Task RemoveRangeAsync(TimeSpan roomOutDate, TimeSpan roundOutDate) + { + var currentDate = DateTimeOffset.UtcNow.Ticks; + + var rooms = await _repository.Rooms + .Include(room => room.Round) + .Where(room => room.CreationTimeTicks + roomOutDate.Ticks < currentDate && room.Round == null) + .ToArrayAsync(); + + var roomLength = rooms.Length; + + var allRounds = await _repository.Rounds + .Where(round => round.FinishTimeTicks + roundOutDate.Ticks < currentDate) + .ToArrayAsync(); + + var roundsLength = allRounds.Length; + + if (roomLength is not 0) + { + _repository.Rooms.RemoveRange(rooms); + } + + if (roundsLength is not 0) + { + _repository.Rounds.RemoveRange(allRounds); + } + + if (roundsLength is not 0 && roomLength is not 0) + { + await _repository.SaveChangesAsync(); + } + + return roomLength + roundsLength; + } + + private async Task> GetPrivateAsync(string userId, string roomCode) + { + var room = await _repository.Rooms + .Include(room => room.Players) + .FirstOrDefaultAsync(room => room.Code == roomCode); + + if (room is null) + { + return new CustomException(ExceptionTemplates.NotExists(nameof(Room))); + } + + if (room.IsFull) + { + return new CustomException(ExceptionTemplates.RoomFull); + } + + if (room.Players.Any(player => player.AccountId == userId)) + { + return new CustomException(ExceptionTemplates.AlreadyInRoom); + } + + return room; + } + + private async Task> GetPublicAsync(string userId) + { + var room = await _repository.Rooms + .Include(room => room.Players) + .FirstOrDefaultAsync(room => !room.IsFull && !room.IsPrivate && room.Players.All(player => player.AccountId != userId)); + + if (room is null) + { + return new CustomException("There are no available free rooms"); + } + + return room; + } +} \ No newline at end of file diff --git a/Server/Server.Bll/Services/RoundService.cs b/Server/Server.Bll/Services/RoundService.cs new file mode 100644 index 0000000..398d519 --- /dev/null +++ b/Server/Server.Bll/Services/RoundService.cs @@ -0,0 +1,145 @@ +using Microsoft.EntityFrameworkCore; +using RockPaperScissors.Common; +using Server.Bll.Services.Interfaces; +using Server.Data.Context; +using Server.Data.Entities; +using Server.Data.Extensions; +using OneOf; +using RockPaperScissors.Common.Enums; +using PlayerState = Server.Data.Entities.PlayerState; + +namespace Server.Bll.Services; + +internal sealed class RoundService: IRoundService +{ + private readonly ServerContext _serverContext; + + public RoundService(ServerContext serverContext) + { + _serverContext = serverContext ?? throw new ArgumentNullException(nameof(serverContext)); + } + + public async Task> MakeMoveAsync(string userId, string roundId, Move move) + { + var round = await _serverContext.Rounds + .Include(round => round.Players) + .Include(round => round.Room) + .FirstOrDefaultAsync(round => round.Id == roundId); + + if (round is null) + { + return new CustomException($"Unable to find round with id '{roundId}'"); + } + + if (round.IsFinished) + { + return new CustomException($"Round has been finished."); + } + + var updateTicks = DateTimeOffset.UtcNow.Ticks; + ProcessMoves(round, userId, move); + + round.UpdateTicks = updateTicks; + round.Room.UpdateTicks = updateTicks; + + _serverContext.Update(round); + + await _serverContext.SaveChangesAsync(); + + return true; + } + + public async Task GetUpdateTicksAsync(string roundId) + { + var round = await _serverContext.Rounds + .FirstOrDefaultAsync(rounds => rounds.Id== roundId); + + return round?.UpdateTicks ?? -1; + } + + private void ProcessMoves(Round round, string userId, Move move) + { + var players = round.Players; + var playingPlayer = players.FirstOrDefault(player => player.AccountId == userId); + + if (playingPlayer is null) + { + return; + } + + var otherPlayer = players.First(player => player.AccountId != userId); + + if (otherPlayer.AccountId == SeedingExtension.BotId) + { + otherPlayer.Move = Random.Shared.Next(1, Enum.GetNames().Length); + } + + playingPlayer.Move = (int)move; + + if (otherPlayer.Move is (int)Move.None) + { + return; + } + + var playingPlayerMove = (Move)playingPlayer.Move; + var otherPlayerMove = (Move)otherPlayer.Move; + + playingPlayer.PlayerState = playingPlayerMove switch + { + Move.Paper => otherPlayerMove switch + { + Move.Rock => PlayerState.Win, + Move.Scissors => PlayerState.Lose, + Move.Paper => PlayerState.Draw, + _ => PlayerState.None, + }, + Move.Rock => otherPlayerMove switch + { + Move.Rock => PlayerState.Draw, + Move.Scissors => PlayerState.Win, + Move.Paper => PlayerState.Lose, + _ => PlayerState.None, + }, + Move.Scissors => otherPlayerMove switch + { + Move.Rock => PlayerState.Lose, + Move.Scissors => PlayerState.Draw, + Move.Paper => PlayerState.Win, + _ => PlayerState.None, + }, + _ => PlayerState.None, + }; + + otherPlayer.PlayerState = playingPlayer.PlayerState switch + { + PlayerState.Win => PlayerState.Lose, + PlayerState.Lose => PlayerState.Win, + PlayerState.Draw => PlayerState.Draw, + _ => PlayerState.None, + }; + + if (playingPlayer.PlayerState is not PlayerState.None && otherPlayer.PlayerState is not PlayerState.None) + { + round.IsFinished = true; + } + } + + public static Round Create(Room room) + { + var currentTime = DateTimeOffset.UtcNow.Ticks; + var newRound = new Round + { + Id = Guid.NewGuid().ToString(), + RoomId = room.Id, + Room = room, + Players = room.Players, + IsFinished = false, + StartTimeTicks = currentTime, + FinishTimeTicks = 0, + UpdateTicks = currentTime + }; + + return newRound; + } + +} \ No newline at end of file diff --git a/Server/Server.Bll/Services/StatisticsService.cs b/Server/Server.Bll/Services/StatisticsService.cs new file mode 100644 index 0000000..455ed35 --- /dev/null +++ b/Server/Server.Bll/Services/StatisticsService.cs @@ -0,0 +1,44 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using RockPaperScissors.Common; +using Server.Bll.Models; +using Server.Bll.Services.Interfaces; +using OneOf; +using Server.Data.Context; + +namespace Server.Bll.Services; + +internal sealed class StatisticsService : IStatisticsService +{ + private readonly ServerContext _repository; + + public StatisticsService(ServerContext repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public Task GetAllAsync() + { + return _repository + .StatisticsEnumerable + .Include(statistics => statistics.Account) + .OrderByDescending(statistics => statistics.Score) + .Take(10) + .ProjectToType() + .ToArrayAsync(); + } + + public async Task> GetAsync(string userId) + { + var statistics = await _repository.StatisticsEnumerable + .Include(stats => stats.Account) + .FirstOrDefaultAsync(statistics => statistics.Id.Equals(userId)); + + if (statistics is null) + { + return new CustomException($"Unable to get statistics for user \"{userId}\""); + } + + return statistics.Adapt(); + } +} \ No newline at end of file diff --git a/Server/Server.Data/Context/ServerContext.cs b/Server/Server.Data/Context/ServerContext.cs new file mode 100644 index 0000000..295eaeb --- /dev/null +++ b/Server/Server.Data/Context/ServerContext.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using Server.Data.Entities; +using Server.Data.Extensions; + +namespace Server.Data.Context; + +public sealed class ServerContext : DbContext +{ + public DbSet Accounts { get; init; } + + public DbSet Rooms { get; init; } + + + public DbSet Players { get; init; } + + public DbSet Rounds { get; init; } + + public DbSet StatisticsEnumerable { get; init; } + + public ServerContext(DbContextOptions contextOptions) + :base(contextOptions) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasQueryFilter(round => !round.IsFinished); + + modelBuilder.Entity() + .HasQueryFilter(statistics => statistics.AccountId != SeedingExtension.BotId); + } +} \ No newline at end of file diff --git a/Server/Server.Data/Entities/Account.cs b/Server/Server.Data/Entities/Account.cs new file mode 100644 index 0000000..d6467e9 --- /dev/null +++ b/Server/Server.Data/Entities/Account.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Data.Entities; + +[Table(nameof(Account))] +public class Account +{ + /// + /// Id of account. Unique to everyone and similar with Statistics Id + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Id { get; init; } + + /// + /// Nick name of Account. + /// + public string Login { get; init; } + + /// + /// Password of the Account + /// + public string Password { get; init; } + + /// + /// Linked to this player statistics + /// + public virtual Statistics Statistics { get; set; } +} \ No newline at end of file diff --git a/Server/Server.Data/Entities/Player.cs b/Server/Server.Data/Entities/Player.cs new file mode 100644 index 0000000..37911ae --- /dev/null +++ b/Server/Server.Data/Entities/Player.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Data.Entities; + +[Table(nameof(Player))] +public class Player +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Id { get; init; } + + [ForeignKey(nameof(Account))] + public string AccountId { get; init; } + + public virtual Account Account { get; set; } + + public bool IsReady { get; set; } + + public int Move { get; set; } + + public PlayerState PlayerState { get; set; } +} \ No newline at end of file diff --git a/Server/Server.Data/Entities/PlayerState.cs b/Server/Server.Data/Entities/PlayerState.cs new file mode 100644 index 0000000..ed2d8a8 --- /dev/null +++ b/Server/Server.Data/Entities/PlayerState.cs @@ -0,0 +1,12 @@ +namespace Server.Data.Entities; + +public enum PlayerState +{ + None = 0, + + Lose = 1, + + Win = 2, + + Draw = 3, +} \ No newline at end of file diff --git a/Server/Server.Data/Entities/Room.cs b/Server/Server.Data/Entities/Room.cs new file mode 100644 index 0000000..9323e9d --- /dev/null +++ b/Server/Server.Data/Entities/Room.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Data.Entities; + +[Table(nameof(Room))] +public class Room +{ + /// + /// Id of the room. Consists of 5 randomized chars + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Id { get; init; } + + /// + /// Special code to join a room + /// + public string Code { get; init; } + + /// + /// Round, linked to this room + /// + public virtual Round Round { get; set; } + + /// + /// . + /// + public virtual ICollection Players { get; set; } + + /// + /// Flag is this room is private + /// + public bool IsPrivate { get; set; } + + /// + /// Flag if room is full + /// + public bool IsFull { get; set; } + + /// + /// Creation date. After 5 minutes of inactivity will be deleted + /// + public long CreationTimeTicks { get; set; } + + /// + /// Last update time ticks. + /// + public long UpdateTicks { get; set; } +} \ No newline at end of file diff --git a/Server/Server.Data/Entities/Round.cs b/Server/Server.Data/Entities/Round.cs new file mode 100644 index 0000000..495f2ea --- /dev/null +++ b/Server/Server.Data/Entities/Round.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Data.Entities; + +[Table(nameof(Round))] +public class Round +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Id { get; init; } + + [ForeignKey(nameof(Room))] + public string RoomId { get; set; } + + public virtual Room Room { get; set; } + + public virtual ICollection Players { get; set; } + + public bool IsFinished { get; set; } + + public bool IsDraw { get; init; } + + public long StartTimeTicks { get; set; } + + public long FinishTimeTicks { get; set; } + + public long UpdateTicks { get; set; } +} \ No newline at end of file diff --git a/Server/Server.Data/Entities/Statistics.cs b/Server/Server.Data/Entities/Statistics.cs new file mode 100644 index 0000000..a40b109 --- /dev/null +++ b/Server/Server.Data/Entities/Statistics.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Data.Entities; + +[Table(nameof(Statistics))] +public class Statistics +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Id { get; init; } + + [ForeignKey(nameof(Account))] + public string AccountId { get; set; } + + public virtual Account Account { get; set; } + + public int Wins { get; set; } + + public int Loss { get; set; } + + public int Draws { get; set; } + + public double WinLossRatio { get; set; } + + public TimeSpan TimeSpent { get; set; } + + public int UsedRock { get; set; } + + public int UsedPaper { get; set; } + + public int UsedScissors { get; set; } + + public int Score { get; set; } +} \ No newline at end of file diff --git a/Server/Server.Data/Extensions/DatabaseExtension.cs b/Server/Server.Data/Extensions/DatabaseExtension.cs new file mode 100644 index 0000000..9f8f3b1 --- /dev/null +++ b/Server/Server.Data/Extensions/DatabaseExtension.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Server.Data.Context; + +namespace Server.Data.Extensions; + +public static class DatabaseExtension +{ + private const string DatabaseConnection = "DatabaseConnection"; + + public static IServiceCollection AddDatabase(this IServiceCollection service, IConfiguration configuration) + { + return service.AddDbContext( + builder => builder.UseSqlite + (configuration.GetConnectionString(DatabaseConnection), + optionsBuilder => optionsBuilder.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName)), + ServiceLifetime.Transient); + } +} \ No newline at end of file diff --git a/Server/Server.Data/Extensions/SeedingExtension.cs b/Server/Server.Data/Extensions/SeedingExtension.cs new file mode 100644 index 0000000..6756b96 --- /dev/null +++ b/Server/Server.Data/Extensions/SeedingExtension.cs @@ -0,0 +1,47 @@ +using Server.Data.Context; +using Server.Data.Entities; + +namespace Server.Data.Extensions; + +public static class SeedingExtension +{ + public static readonly Player BotPlayer = new() + { + Id = Guid.NewGuid().ToString(), + AccountId = BotId, + IsReady = true + }; + + public const string BotId = "bot"; + + public static async Task EnsureBotCreated(this ServerContext context) + { + var bot = await context.Accounts.FindAsync(BotId); + if (bot is not null) + { + return; + } + + var botAccount = new Account + { + Id = BotId, + Login = BotId, + Password = Guid.NewGuid().ToString() + }; + + context.Add(botAccount); + + BotPlayer.Account = botAccount; + + context.Add(new Statistics + { + Id = BotId, + Account = botAccount, + AccountId = BotId + }); + + context.Add(BotPlayer); + + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/Server/Server.Data/Migrations/20221023190847_InitialMigration.Designer.cs b/Server/Server.Data/Migrations/20221023190847_InitialMigration.Designer.cs new file mode 100644 index 0000000..5aa848d --- /dev/null +++ b/Server/Server.Data/Migrations/20221023190847_InitialMigration.Designer.cs @@ -0,0 +1,225 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Server.Data.Context; + +#nullable disable + +namespace Server.Data.Migrations +{ + [DbContext(typeof(ServerContext))] + [Migration("20221023190847_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + + modelBuilder.Entity("Server.Data.Entities.Account", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Login") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("Server.Data.Entities.Player", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("IsReady") + .HasColumnType("INTEGER"); + + b.Property("Move") + .HasColumnType("INTEGER"); + + b.Property("PlayerState") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("RoomId"); + + b.HasIndex("RoundId"); + + b.ToTable("Player"); + }); + + modelBuilder.Entity("Server.Data.Entities.Room", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Code") + .HasColumnType("TEXT"); + + b.Property("CreationTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("IsFull") + .HasColumnType("INTEGER"); + + b.Property("IsPrivate") + .HasColumnType("INTEGER"); + + b.Property("UpdateTicks") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Room"); + }); + + modelBuilder.Entity("Server.Data.Entities.Round", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FinishTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("IsDraw") + .HasColumnType("INTEGER"); + + b.Property("IsFinished") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("TEXT"); + + b.Property("StartTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("UpdateTicks") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Round"); + }); + + modelBuilder.Entity("Server.Data.Entities.Statistics", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("Draws") + .HasColumnType("INTEGER"); + + b.Property("Loss") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("TimeSpent") + .HasColumnType("TEXT"); + + b.Property("UsedPaper") + .HasColumnType("INTEGER"); + + b.Property("UsedRock") + .HasColumnType("INTEGER"); + + b.Property("UsedScissors") + .HasColumnType("INTEGER"); + + b.Property("WinLossRatio") + .HasColumnType("REAL"); + + b.Property("Wins") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId") + .IsUnique(); + + b.ToTable("Statistics"); + }); + + modelBuilder.Entity("Server.Data.Entities.Player", b => + { + b.HasOne("Server.Data.Entities.Account", "Account") + .WithMany() + .HasForeignKey("AccountId"); + + b.HasOne("Server.Data.Entities.Room", null) + .WithMany("Players") + .HasForeignKey("RoomId"); + + b.HasOne("Server.Data.Entities.Round", null) + .WithMany("Players") + .HasForeignKey("RoundId"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Server.Data.Entities.Round", b => + { + b.HasOne("Server.Data.Entities.Room", "Room") + .WithOne("Round") + .HasForeignKey("Server.Data.Entities.Round", "RoomId"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Server.Data.Entities.Statistics", b => + { + b.HasOne("Server.Data.Entities.Account", "Account") + .WithOne("Statistics") + .HasForeignKey("Server.Data.Entities.Statistics", "AccountId"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Server.Data.Entities.Account", b => + { + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("Server.Data.Entities.Room", b => + { + b.Navigation("Players"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Server.Data.Entities.Round", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Server.Data/Migrations/20221023190847_InitialMigration.cs b/Server/Server.Data/Migrations/20221023190847_InitialMigration.cs new file mode 100644 index 0000000..15c3228 --- /dev/null +++ b/Server/Server.Data/Migrations/20221023190847_InitialMigration.cs @@ -0,0 +1,167 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Server.Data.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Account", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Login = table.Column(type: "TEXT", nullable: true), + Password = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Account", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Room", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Code = table.Column(type: "TEXT", nullable: true), + IsPrivate = table.Column(type: "INTEGER", nullable: false), + IsFull = table.Column(type: "INTEGER", nullable: false), + CreationTimeTicks = table.Column(type: "INTEGER", nullable: false), + UpdateTicks = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Room", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Statistics", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: true), + Wins = table.Column(type: "INTEGER", nullable: false), + Loss = table.Column(type: "INTEGER", nullable: false), + Draws = table.Column(type: "INTEGER", nullable: false), + WinLossRatio = table.Column(type: "REAL", nullable: false), + TimeSpent = table.Column(type: "TEXT", nullable: false), + UsedRock = table.Column(type: "INTEGER", nullable: false), + UsedPaper = table.Column(type: "INTEGER", nullable: false), + UsedScissors = table.Column(type: "INTEGER", nullable: false), + Score = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Statistics", x => x.Id); + table.ForeignKey( + name: "FK_Statistics_Account_AccountId", + column: x => x.AccountId, + principalTable: "Account", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Round", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + RoomId = table.Column(type: "TEXT", nullable: true), + IsFinished = table.Column(type: "INTEGER", nullable: false), + IsDraw = table.Column(type: "INTEGER", nullable: false), + StartTimeTicks = table.Column(type: "INTEGER", nullable: false), + FinishTimeTicks = table.Column(type: "INTEGER", nullable: false), + UpdateTicks = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Round", x => x.Id); + table.ForeignKey( + name: "FK_Round_Room_RoomId", + column: x => x.RoomId, + principalTable: "Room", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Player", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: true), + IsReady = table.Column(type: "INTEGER", nullable: false), + Move = table.Column(type: "INTEGER", nullable: false), + PlayerState = table.Column(type: "INTEGER", nullable: false), + RoomId = table.Column(type: "TEXT", nullable: true), + RoundId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Player", x => x.Id); + table.ForeignKey( + name: "FK_Player_Account_AccountId", + column: x => x.AccountId, + principalTable: "Account", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Player_Room_RoomId", + column: x => x.RoomId, + principalTable: "Room", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Player_Round_RoundId", + column: x => x.RoundId, + principalTable: "Round", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Player_AccountId", + table: "Player", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_Player_RoomId", + table: "Player", + column: "RoomId"); + + migrationBuilder.CreateIndex( + name: "IX_Player_RoundId", + table: "Player", + column: "RoundId"); + + migrationBuilder.CreateIndex( + name: "IX_Round_RoomId", + table: "Round", + column: "RoomId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Statistics_AccountId", + table: "Statistics", + column: "AccountId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Player"); + + migrationBuilder.DropTable( + name: "Statistics"); + + migrationBuilder.DropTable( + name: "Round"); + + migrationBuilder.DropTable( + name: "Account"); + + migrationBuilder.DropTable( + name: "Room"); + } + } +} diff --git a/Server/Server.Data/Migrations/ServerContextModelSnapshot.cs b/Server/Server.Data/Migrations/ServerContextModelSnapshot.cs new file mode 100644 index 0000000..6220117 --- /dev/null +++ b/Server/Server.Data/Migrations/ServerContextModelSnapshot.cs @@ -0,0 +1,223 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Server.Data.Context; + +#nullable disable + +namespace Server.Data.Migrations +{ + [DbContext(typeof(ServerContext))] + partial class ServerContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + + modelBuilder.Entity("Server.Data.Entities.Account", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Login") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("Server.Data.Entities.Player", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("IsReady") + .HasColumnType("INTEGER"); + + b.Property("Move") + .HasColumnType("INTEGER"); + + b.Property("PlayerState") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("RoomId"); + + b.HasIndex("RoundId"); + + b.ToTable("Player"); + }); + + modelBuilder.Entity("Server.Data.Entities.Room", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Code") + .HasColumnType("TEXT"); + + b.Property("CreationTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("IsFull") + .HasColumnType("INTEGER"); + + b.Property("IsPrivate") + .HasColumnType("INTEGER"); + + b.Property("UpdateTicks") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Room"); + }); + + modelBuilder.Entity("Server.Data.Entities.Round", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FinishTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("IsDraw") + .HasColumnType("INTEGER"); + + b.Property("IsFinished") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("TEXT"); + + b.Property("StartTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("UpdateTicks") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Round"); + }); + + modelBuilder.Entity("Server.Data.Entities.Statistics", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("Draws") + .HasColumnType("INTEGER"); + + b.Property("Loss") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("TimeSpent") + .HasColumnType("TEXT"); + + b.Property("UsedPaper") + .HasColumnType("INTEGER"); + + b.Property("UsedRock") + .HasColumnType("INTEGER"); + + b.Property("UsedScissors") + .HasColumnType("INTEGER"); + + b.Property("WinLossRatio") + .HasColumnType("REAL"); + + b.Property("Wins") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId") + .IsUnique(); + + b.ToTable("Statistics"); + }); + + modelBuilder.Entity("Server.Data.Entities.Player", b => + { + b.HasOne("Server.Data.Entities.Account", "Account") + .WithMany() + .HasForeignKey("AccountId"); + + b.HasOne("Server.Data.Entities.Room", null) + .WithMany("Players") + .HasForeignKey("RoomId"); + + b.HasOne("Server.Data.Entities.Round", null) + .WithMany("Players") + .HasForeignKey("RoundId"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Server.Data.Entities.Round", b => + { + b.HasOne("Server.Data.Entities.Room", "Room") + .WithOne("Round") + .HasForeignKey("Server.Data.Entities.Round", "RoomId"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Server.Data.Entities.Statistics", b => + { + b.HasOne("Server.Data.Entities.Account", "Account") + .WithOne("Statistics") + .HasForeignKey("Server.Data.Entities.Statistics", "AccountId"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Server.Data.Entities.Account", b => + { + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("Server.Data.Entities.Room", b => + { + b.Navigation("Players"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Server.Data.Entities.Round", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Server.Data/Server.Data.csproj b/Server/Server.Data/Server.Data.csproj new file mode 100644 index 0000000..176511f --- /dev/null +++ b/Server/Server.Data/Server.Data.csproj @@ -0,0 +1,16 @@ + + + + net6 + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Server/Server.Host/Controllers/AccountController.cs b/Server/Server.Host/Controllers/AccountController.cs new file mode 100644 index 0000000..6609bd7 --- /dev/null +++ b/Server/Server.Host/Controllers/AccountController.cs @@ -0,0 +1,57 @@ +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RockPaperScissors.Common; +using RockPaperScissors.Common.Models; +using RockPaperScissors.Common.Requests; +using Server.Authentication.Exceptions; +using Server.Authentication.Models; +using Server.Authentication.Services; + +namespace Server.Host.Controllers; + +public sealed class AccountController: ControllerBase +{ + private readonly IAuthService _authService; + + public AccountController(IAuthService authService) + { + _authService = authService ?? throw new ArgumentNullException(nameof(authService)); + } + + [AllowAnonymous] + [HttpPost(UrlTemplates.RegisterUrl)] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(UserException),StatusCodes.Status400BadRequest)] + public async Task RegisterAsync(RegisterRequest registerRequest) + { + var newAccount = await _authService + .RegisterAsync(registerRequest.Login,registerRequest.Password); + + return newAccount.Match( + statusCode => Ok(statusCode.ToString()), + BadRequest); + } + + [AllowAnonymous] + [HttpPost(UrlTemplates.LoginUrl)] + [ProducesResponseType(typeof(AccountOutputModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(UserException),StatusCodes.Status400BadRequest)] + public async Task LoginAsync(AccountDto accountDto) + { + var newAccount = + await _authService.LoginAsync(accountDto.Login, accountDto.Password); + + return newAccount.Match( + Ok, + BadRequest); + } + + [HttpGet(UrlTemplates.LogoutUrl)] + [ProducesResponseType(typeof(int), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(int), (int) HttpStatusCode.BadRequest)] + public ActionResult Logout(string sessionId) + { + return sessionId; + } +} \ No newline at end of file diff --git a/Server/Server.Host/Controllers/ControllerBase.cs b/Server/Server.Host/Controllers/ControllerBase.cs new file mode 100644 index 0000000..6251b03 --- /dev/null +++ b/Server/Server.Host/Controllers/ControllerBase.cs @@ -0,0 +1,15 @@ +using System.Net.Mime; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Server.Host.Controllers; + +[ApiController] +[Consumes(MediaTypeNames.Application.Json)] +[Produces(MediaTypeNames.Application.Json)] +[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +public class ControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase +{ + protected string UserId => User.Identity?.Name ?? string.Empty; +} \ No newline at end of file diff --git a/Server/Server.Host/Controllers/RoomController.cs b/Server/Server.Host/Controllers/RoomController.cs new file mode 100644 index 0000000..a8d5102 --- /dev/null +++ b/Server/Server.Host/Controllers/RoomController.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Mvc; +using RockPaperScissors.Common; +using Server.Bll.Models; +using Server.Bll.Services.Interfaces; + +namespace Server.Host.Controllers; + +public sealed class RoomController: ControllerBase +{ + private readonly IRoomService _roomService; + + public RoomController(IRoomService roomService) + { + _roomService = roomService ?? throw new ArgumentNullException(nameof(roomService)); + } + + [HttpPost(UrlTemplates.CreateRoom)] + [ProducesResponseType(typeof(RoomModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task CreateAsync( + [FromQuery] bool isPrivate, + [FromQuery] bool isTraining = false) + { + var newRoom = await _roomService + .CreateAsync(UserId, isPrivate, isTraining); + + return newRoom.Match( + Ok, + BadRequest); + } + + [HttpPost(UrlTemplates.JoinPublicRoom)] + [ProducesResponseType(typeof(RoomModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task JoinPublicAsync() + { + var result = await _roomService.JoinAsync(UserId); + + return result.Match( + Ok, + BadRequest); + } + + [HttpPost(UrlTemplates.JoinPrivateRoom)] + [ProducesResponseType(typeof(RoomModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task JoinPrivateAsync(string roomCode) + { + var result = await _roomService.JoinAsync(UserId, roomCode); + + return result.Match( + Ok, + BadRequest); + } + + [HttpPost(UrlTemplates.DeleteRoom)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task DeleteAsync(string roomId) + { + var deleteResponse = await _roomService.DeleteAsync(UserId, roomId); + + return deleteResponse.Match( + _ => Ok(), + BadRequest); + } + + [HttpPost(UrlTemplates.ChangeStatus)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task ChangePlayerStatusAsync(string roomId, [FromQuery] bool newStatus) + { + var changePlayerStatus = await _roomService.ChangePlayerStatusAsync(UserId, roomId, newStatus); + + return changePlayerStatus.Match( + Ok, + BadRequest); + } + + [HttpGet(UrlTemplates.CheckRoomUpdateTicks)] + [ProducesResponseType(typeof(long), StatusCodes.Status200OK)] + public Task CheckUpdateTicksAsync(string roomId) + { + return _roomService.GetUpdateTicksAsync(roomId); + } +} \ No newline at end of file diff --git a/Server/Server.Host/Controllers/RoundController.cs b/Server/Server.Host/Controllers/RoundController.cs new file mode 100644 index 0000000..75cb410 --- /dev/null +++ b/Server/Server.Host/Controllers/RoundController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using RockPaperScissors.Common; +using RockPaperScissors.Common.Enums; +using Server.Bll.Services.Interfaces; + +namespace Server.Host.Controllers; + +/// +/// API Round Controller +/// +public sealed class RoundController: ControllerBase +{ + private readonly IRoundService _roundService; + + public RoundController(IRoundService roundService) + { + _roundService = roundService ?? throw new ArgumentNullException(nameof(roundService)); + } + + [HttpGet(UrlTemplates.CheckRoundUpdateTicks)] + [ProducesResponseType(typeof(long), StatusCodes.Status200OK)] + public Task CheckUpdateTicksAsync(string roundId) + { + return _roundService.GetUpdateTicksAsync(roundId); + } + + [HttpGet(UrlTemplates.MakeMove)] + [ProducesResponseType(typeof(long), StatusCodes.Status200OK)] + public async Task MakeMoveAsync(string roundId, Move move) + { + var makeMove = await _roundService.MakeMoveAsync(UserId, roundId, move); + + return makeMove.Match(_ => Ok(), BadRequest); + } +} \ No newline at end of file diff --git a/Server/Server.Host/Controllers/StatisticsController.cs b/Server/Server.Host/Controllers/StatisticsController.cs new file mode 100644 index 0000000..fe6461f --- /dev/null +++ b/Server/Server.Host/Controllers/StatisticsController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RockPaperScissors.Common; +using Server.Bll.Models; +using Server.Bll.Services.Interfaces; + +namespace Server.Host.Controllers; + +public sealed class StatisticsController: ControllerBase +{ + private readonly IStatisticsService _statisticsService; + + public StatisticsController(IStatisticsService statisticsService) + { + _statisticsService = statisticsService ?? throw new ArgumentNullException(nameof(statisticsService)); + } + + [AllowAnonymous] + [HttpGet(UrlTemplates.AllStatistics)] + [ProducesResponseType(typeof(ShortStatisticsModel[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public Task GetOverallStatistics() + { + return _statisticsService.GetAllAsync(); + } + + [Authorize] + [HttpGet(UrlTemplates.PersonalStatistics)] + [ProducesResponseType(typeof(StatisticsModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(CustomException), StatusCodes.Status400BadRequest)] + public async Task GetPersonalStatistics() + { + var result = await _statisticsService.GetAsync(UserId); + + return result.Match( + Ok, + BadRequest); + } +} \ No newline at end of file diff --git a/Server/Server.Host/Extensions/LoggingMiddleware.cs b/Server/Server.Host/Extensions/LoggingMiddleware.cs new file mode 100644 index 0000000..f2dbdf7 --- /dev/null +++ b/Server/Server.Host/Extensions/LoggingMiddleware.cs @@ -0,0 +1,183 @@ +using System.Net.Mime; +using System.Text; + +namespace Server.Host.Extensions; + +internal sealed class LoggingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public LoggingMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory) + { + _next = next; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + var requestInformation = await BuildLogAsync(context); + + _logger.LogWarning(requestInformation); + + var originalResponseBody = context.Response.Body; + await using var responseBody = new MemoryStream(); + + context.Response.Body = responseBody; + await _next(context); + + var status = GetStatusCode(context); + var level = GetLogLevel(status); + + _logger.Log(level, "Response body: LogLevel: {Enum}; Code: {Status}\n Body: {Body}", + Enum.GetName(GetLogLevel(status)), status, await ObtainResponseBodyAsync(context)); + + await responseBody.CopyToAsync(originalResponseBody); + + } + + private static async Task ObtainRequestBodyAsync(HttpRequest request) + { + request.EnableBuffering(); + var encoding = GetEncodingFromContentType(request.ContentType); + string bodyStr; + + using (var reader = new StreamReader(request.Body, encoding, true, 1024, true)) + { + bodyStr = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + request.Body.Seek(0, SeekOrigin.Begin); + return bodyStr; + } + + private static async Task ObtainResponseBodyAsync(HttpContext context) + { + var response = context.Response; + response.Body.Seek(0, SeekOrigin.Begin); + + var encoding = GetEncodingFromContentType(response.ContentType); + + using var reader = new StreamReader(response.Body, encoding, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true); + + var text = await reader.ReadToEndAsync().ConfigureAwait(false); + response.Body.Seek(0, SeekOrigin.Begin); + + return text; + } + + private static Encoding GetEncodingFromContentType(string? contentTypeStr) + { + if (string.IsNullOrEmpty(contentTypeStr)) + { + return Encoding.UTF8; + } + ContentType contentType; + + try + { + contentType = new ContentType(contentTypeStr); + } + catch (FormatException) + { + return Encoding.UTF8; + } + + return string.IsNullOrEmpty(contentType.CharSet) + ? Encoding.UTF8 + : Encoding.GetEncoding(contentType.CharSet, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); + } + private static LogLevel GetLogLevel(int? statusCode) + { + return statusCode > 399 ? LogLevel.Error : LogLevel.Information; + } + private static int? GetStatusCode(HttpContext context) + { + return context.Response.StatusCode; + } + + private static async Task BuildLogAsync(HttpContext context) + { + var requestBody = await ObtainRequestBodyAsync(context.Request); + + var length = 89 + + context.Request.Scheme.Length + + context.Request.ContentType?.Length + + context.Request.Host.Host.Length + + (context.Request.Path.HasValue ? context.Request.Path.Value.Length : 0) + + (context.Request.QueryString.HasValue ? context.Request.QueryString.Value!.Length : 0) + + requestBody.Length ?? 0; + + if (length <= 88) + { + return string.Empty; + } + + return string.Create(length, (context, requestBody), (span, tuple) => + { + var index = 0; + + var (thisContext, thisRequestBody) = tuple; + + var tempString = "Request information:\n"; + tempString.CopyTo(span[index..]); + index += tempString.Length; + + tempString = "Schema:"; + tempString.CopyTo(span[index..]); + index += tempString.Length; + + thisContext.Request.Scheme.CopyTo(span[index..]); + index += thisContext.Request.Scheme.Length; + + "\n".CopyTo(span[index++..]); + + tempString = "Content-Type:"; + tempString.CopyTo(span[index..]); + index += tempString.Length; + + thisContext.Request.ContentType?.CopyTo(span[index..]); + index += thisContext.Request.ContentType?.Length ?? 0; + + "\n".CopyTo(span[index++..]); + + tempString = "Host:"; + tempString.CopyTo(span[index..]); + index += tempString.Length; + + thisContext.Request.Host.Host.CopyTo(span[index..]); + index += thisContext.Request.Host.Host.Length; + + "\n".CopyTo(span[index++..]); + + tempString = "Path:"; + tempString.CopyTo(span[index..]); + index += tempString.Length; + + thisContext.Request.Path.Value?.CopyTo(span[index..]); + index += thisContext.Request.Path.Value?.Length ?? 0; + + "\n".CopyTo(span[index++..]); + + tempString = "Query String:"; + tempString.CopyTo(span[index..]); + index += tempString.Length; + + thisContext.Request.QueryString.Value?.CopyTo(span[index..]); + index += thisContext.Request.QueryString.Value?.Length ?? 0; + + "\n".CopyTo(span[index++..]); + + tempString = "Request Body:"; + tempString.CopyTo(span[index..]); + index += tempString.Length; + + thisRequestBody.CopyTo(span[index..]); + index += thisRequestBody.Length; + + "\n".CopyTo(span[index..]); + }); + } +} \ No newline at end of file diff --git a/Server/Server.Host/Extensions/SwaggerExtension.cs b/Server/Server.Host/Extensions/SwaggerExtension.cs new file mode 100644 index 0000000..e822de2 --- /dev/null +++ b/Server/Server.Host/Extensions/SwaggerExtension.cs @@ -0,0 +1,82 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.OpenApi.Models; + +namespace Server.Host.Extensions; + +/// +/// Swagger extension +/// +public static class SwaggerExtension +{ + /// + /// Registers swagger. + /// + /// Service collection. + /// Service collection. + public static IServiceCollection AddSwagger(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + services.AddSwaggerGen(options => + { + var title = AppDomain.CurrentDomain.FriendlyName; + var assemblyName = Assembly.GetExecutingAssembly().GetName(); + var version = assemblyName.Version?.ToString() ?? string.Empty; + var documentationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{assemblyName.Name}.xml"); + + options.IncludeXmlComments(documentationPath); + options.SwaggerDoc(version, new OpenApiInfo + { + Title = title, + Version = version + }); + + var jwtSecurityScheme = new OpenApiSecurityScheme + { + BearerFormat = JwtConstants.TokenType, + Name = "JWT Authentication", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = JwtBearerDefaults.AuthenticationScheme, + Description = "Put **_ONLY_** your JWT Bearer token on text box below!", + + Reference = new OpenApiReference + { + Id = JwtBearerDefaults.AuthenticationScheme, + Type = ReferenceType.SecurityScheme + } + }; + + options.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + jwtSecurityScheme, + Array.Empty() + } + }); + }); + + return services; + } + + public static IApplicationBuilder UseSwaggerUI( + this IApplicationBuilder applicationBuilder) + { + ArgumentNullException.ThrowIfNull(applicationBuilder); + + applicationBuilder.UseSwaggerUI(swaggerUiOptions => + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; + var title = $"{AppDomain.CurrentDomain.FriendlyName} v{version}"; + + swaggerUiOptions.DocumentTitle = title; + swaggerUiOptions.SwaggerEndpoint($"/swagger/{version}/swagger.json", title); + }); + + return applicationBuilder; + } +} \ No newline at end of file diff --git a/Server/Server.Host/Program.cs b/Server/Server.Host/Program.cs new file mode 100644 index 0000000..b6aab50 --- /dev/null +++ b/Server/Server.Host/Program.cs @@ -0,0 +1,29 @@ +using Serilog; + +namespace Server.Host; + +public static class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args) + .ConfigureLogging(loggingBuilder => + { + loggingBuilder.ClearProviders(); + loggingBuilder.SetMinimumLevel(LogLevel.Information); + loggingBuilder.AddSerilog(new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger()); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } + +} \ No newline at end of file diff --git a/Server/Properties/launchSettings.json b/Server/Server.Host/Properties/launchSettings.json similarity index 86% rename from Server/Properties/launchSettings.json rename to Server/Server.Host/Properties/launchSettings.json index 604abbb..4fe8a9e 100644 --- a/Server/Properties/launchSettings.json +++ b/Server/Server.Host/Properties/launchSettings.json @@ -1,10 +1,10 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "Server": { + "Server.Host": { "commandName": "Project", "dotnetRunMessages": "true", - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "http://localhost:5000", "environmentVariables": { diff --git a/Server/Server.Host/Server.Host.csproj b/Server/Server.Host/Server.Host.csproj new file mode 100644 index 0000000..8a33ddc --- /dev/null +++ b/Server/Server.Host/Server.Host.csproj @@ -0,0 +1,35 @@ + + + + true + true + net6 + true + enable + 0.0.2 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + diff --git a/Server/Server.Host/Startup.cs b/Server/Server.Host/Startup.cs new file mode 100644 index 0000000..f745838 --- /dev/null +++ b/Server/Server.Host/Startup.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using Server.Authentication.Extensions; +using Server.Bll.Extensions; +using Server.Bll.Options; +using Server.Data.Context; +using Server.Data.Extensions; +using Server.Host.Extensions; + +namespace Server.Host; + +public sealed class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + private IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHealthChecks(); + + services + .Configure(Configuration.GetRequiredSection(CleanerOptions.Section)); + + services + .AddDatabase(Configuration) + .AddSwagger() + .AddAuthentications(); + + services.AddBusinessLogic(); + + services.AddControllers(); + + services.AddCors(); + } + + public static void Configure( + IApplicationBuilder app, + IWebHostEnvironment env, + ServerContext serverContext) + { + serverContext.Database.Migrate(); + serverContext?.EnsureBotCreated(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseCors(builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .WithExposedHeaders("Authorization", "Accept", "Content-Type", "Origin")); + + app.UseMiddleware(); + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHealthChecks("/health"); + endpoints.MapControllers(); + }); + } +} \ No newline at end of file diff --git a/Server/Server.Host/appsettings.json b/Server/Server.Host/appsettings.json new file mode 100644 index 0000000..b1a240e --- /dev/null +++ b/Server/Server.Host/appsettings.json @@ -0,0 +1,18 @@ +{ + "ConnectionStrings": { + "DatabaseConnection": "Data Source=database.db" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Cleaning": { + "CleanPeriod": "00:00:10", + "RoomOutDateTime": "00:05:00", + "RoundOutDateTime": "00:00:30" + }, + "AllowedHosts": "*" +} diff --git a/Server/Server.csproj b/Server/Server.csproj deleted file mode 100644 index cf71a1d..0000000 --- a/Server/Server.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net5.0 - - - - - - - - - - - - - - - - - - diff --git a/Server/Services/AccountManager.cs b/Server/Services/AccountManager.cs deleted file mode 100644 index 904e767..0000000 --- a/Server/Services/AccountManager.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Server.Contracts; -using Server.Exceptions.LogIn; -using Server.Models; -using Server.Services.Interfaces; - -namespace Server.Services -{ - public class AccountManager : IAccountManager - { - private readonly ILogger _logger; - - - private readonly ConcurrentDictionary _invalidTries = new(); - private readonly ConcurrentDictionary _lastTimes = new(); //What am i doing? so stupid - - private readonly IDeserializedObject _deserializedObject; - private const int CoolDownTime = 45; - - public ConcurrentDictionary AccountsActive { get; set; } - - - public AccountManager( - ILogger logger, - IDeserializedObject deserializedObject) - { - _logger = logger; - _deserializedObject = deserializedObject; - AccountsActive = new ConcurrentDictionary(); - } - - /// - /// Method to asynchronously sign in - /// - /// account from client - /// Account on server - /// When too many false retries - /// When invalid data - /// When used is already signed in - public async Task LogInAsync(AccountDto accountDto) - { - var tasks = Task.Factory.StartNew(() => - { - var invalidTryAccount = _invalidTries.FirstOrDefault(x => x.Key == accountDto.SessionId); - - if (invalidTryAccount.Value >= 2) - { - if ((DateTime.Now - _lastTimes.FirstOrDefault(x => x.Key == accountDto.SessionId).Value) - .TotalSeconds >= CoolDownTime) - { - _invalidTries.TryRemove(invalidTryAccount); - } - else - { - // ReSharper disable once RedundantAssignment - _lastTimes.AddOrUpdate(accountDto.SessionId, accountDto.LastRequest, - ((s, time) => time = accountDto.LastRequest)); - throw new LoginCooldownException("CoolDown", CoolDownTime); - } - } - - var login = _deserializedObject.ConcurrentDictionary.Values - .FirstOrDefault(x => x.Login == accountDto.Login && x.Password == accountDto.Password); - - if (login == null) - { - _invalidTries.AddOrUpdate(accountDto.SessionId, 1, (s, i) => i + 1); - - _lastTimes.AddOrUpdate(accountDto.SessionId, accountDto.LastRequest, - ((s, time) => time = accountDto.LastRequest)); - - throw new InvalidCredentialsException($"{accountDto.Login}"); - } - - if (AccountsActive.Any(x => x.Value == login) - || AccountsActive.ContainsKey(accountDto.SessionId)) - - { - var thisAccount = AccountsActive.FirstOrDefault(x => x.Key == accountDto.SessionId).Value; - AccountsActive.TryUpdate(accountDto.SessionId, login, thisAccount); - //throw new UserAlreadySignedInException(nameof(login.Login)); - } - - AccountsActive.TryAdd(accountDto.SessionId, login); - _logger.LogTrace(""); //todo - - return login; - }); - - return await tasks; - } - - /// - /// Async method to sign out of account - /// - /// Session id of client - /// bool - public async Task LogOutAsync(string sessionId) - { - var tasks = Task.Factory - .StartNew(() => AccountsActive.ContainsKey(sessionId) && AccountsActive.TryRemove(sessionId, out _)); - - return await tasks; - - } - - /// - /// Checks if this session is active - /// - /// Id of client session - /// bool - public async Task IsActive(string sessionId) - { - var tasks = Task.Factory.StartNew(() => AccountsActive.ContainsKey(sessionId)); - - return await tasks; - } - - /// - /// Gets active account from list of active accounts by id of client session - /// - /// Id of client session - /// Account - public Account GetActiveAccountBySessionId(string sessionId) - { - AccountsActive.TryGetValue(sessionId, out var account); - - return account; - } - } -} \ No newline at end of file diff --git a/Server/Services/DeserializedObject.cs b/Server/Services/DeserializedObject.cs deleted file mode 100644 index f934d17..0000000 --- a/Server/Services/DeserializedObject.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Collections.Concurrent; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.IO; -using Newtonsoft.Json; -using Server.Models; -using Server.Services.Interfaces; - -namespace Server.Services -{ - public class DeserializedObject : IDeserializedObject where T: class - { - private static string _fileName; - - /// - /// Concurrent Dictionary. Heart of our project - /// - public ConcurrentDictionary ConcurrentDictionary { get; set; } - - public DeserializedObject() - { - ConcurrentDictionary = GetData().Result; - } - - /// - /// Method to retrieve data from Dictionary - /// - /// ConcurrentDictionary - private async Task> GetData() - { - return await Deserialize(); - } - - /// - /// Method to update data in ConcurrentDictionary - /// - /// - public async Task UpdateData() - { - await Serialize(); - } - - /// - /// Check if file is available. Returns true if it exists. Else false - /// - /// bool - private Task IsNeededFilesAvailable() - { - return Task.Run(()=> File.Exists(_fileName)); - } - - /// - /// Method to deserialize data from T file - /// - /// ConcurrentDictionary - private async Task> Deserialize() - { - _fileName = typeof(T).Name.Contains("Statistics") ? "Statistics" : typeof(T).Name; - - var exists = IsNeededFilesAvailable().Result; - - FileStream reader; - if (exists && File.ReadAllTextAsync(_fileName).Result != "") //todo*/ - try - { - byte[] fileText; - await using (reader = File.Open(_fileName, FileMode.Open)) - { - fileText = new byte[reader.Length]; - await reader.ReadAsync(fileText, 0, (int)reader.Length); - } - - var decoded = Encoding.ASCII.GetString(fileText); - - - var list = await Task.Run(() => - JsonConvert.DeserializeObject>(decoded)); - return list; - } - catch (FileNotFoundException exception) - { - File.Create(_fileName); - return new ConcurrentDictionary(); - } - - reader = File.Open(_fileName, FileMode.OpenOrCreate); - reader.Close(); - return new ConcurrentDictionary(); - - } - - /// - /// Method to serialize T objects - /// - /// Void - private async Task Serialize() - { - var streamManager = new RecyclableMemoryStreamManager(); - - using var file = File.Open(_fileName, FileMode.OpenOrCreate); - using var memoryStream = streamManager.GetStream(); - using var writer = new StreamWriter(memoryStream); - - var serializer = JsonSerializer.CreateDefault(); - - serializer.Serialize(writer, ConcurrentDictionary); // FROM STACKOVERFLOW - - await writer.FlushAsync().ConfigureAwait(false); - - memoryStream.Seek(0, SeekOrigin.Begin); - - await memoryStream.CopyToAsync(file).ConfigureAwait(false); - - await file.FlushAsync().ConfigureAwait(false); - - } - } -} \ No newline at end of file diff --git a/Server/Services/Interfaces/IAccountManager.cs b/Server/Services/Interfaces/IAccountManager.cs deleted file mode 100644 index 73fdea6..0000000 --- a/Server/Services/Interfaces/IAccountManager.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Concurrent; -using System.Threading.Tasks; -using Server.Contracts; -using Server.Exceptions.LogIn; -using Server.Models; - -namespace Server.Services.Interfaces -{ - public interface IAccountManager - { - /// - /// List of all active account on the server - /// - ConcurrentDictionary AccountsActive { get; set; } - - /// - /// Method to asynchronously sign in - /// - /// account from client - /// Account on server - /// When too many false retries - /// When invalid data - /// When used is already signed in - Task LogInAsync(AccountDto accountDto); - - /// - /// Async method to sign out of account - /// - /// Session id of client - /// bool - Task LogOutAsync(string sessionId); - - /// - /// Checks if this session is active - /// - /// Id of client session - /// bool - Task IsActive(string sessionId); - - /// - /// Gets active account from list of active accounts by id of client session - /// - /// Id of client session - /// Account - Account GetActiveAccountBySessionId(string sessionId); - } -} \ No newline at end of file diff --git a/Server/Services/Interfaces/IDeserializedObject.cs b/Server/Services/Interfaces/IDeserializedObject.cs deleted file mode 100644 index 46f3b3f..0000000 --- a/Server/Services/Interfaces/IDeserializedObject.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Concurrent; -using System.Threading.Tasks; - -namespace Server.Services.Interfaces -{ - public interface IDeserializedObject - { - /// - /// Concurrent Dictionary. Heart of our project - /// - ConcurrentDictionary ConcurrentDictionary { get; set; } - - /// - /// Method to update data in ConcurrentDictionary - /// - /// - Task UpdateData(); - } -} \ No newline at end of file diff --git a/Server/Services/Interfaces/IStorage.cs b/Server/Services/Interfaces/IStorage.cs deleted file mode 100644 index 2fd1d94..0000000 --- a/Server/Services/Interfaces/IStorage.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Server.Exceptions.Register; - -namespace Server.Services.Interfaces -{ - public interface IStorage where T: class - { - /// - /// Gets all elements from T collection - /// - /// ICollection - ICollection GetAll(); - - /// - /// Gets asynchronously all elements from T collection - /// - /// Task of ICollection - Task> GetAllAsync(); - - /// - /// Gets element by Id - /// - /// Id of an element - /// T item - T Get(string id); - - /// - /// Gets asynchronously element by Id - /// - /// string If of an Element - /// Task T item - Task GetAsync(string id); - - /// - /// Adds T element to the collection - /// - /// T item - /// int - /// This account is already exists - /// Generic for custom user exceptions - int Add(T item); - - /// - /// Adds asynchronously an item to ConcurrentDictionary - /// - /// T item - /// int - Task AddAsync(T item); - - /// - /// Adds or updates item in collection - /// - /// id of item - /// T item - void AddOrUpdate(string id, T item); - - /// - /// Asynchronously adds or updates item in collection - /// - /// id of item - /// T item - Task AddOrUpdateAsync(string id, T item); - - /// - /// Updates value in collection - /// - /// - /// - /// Task - Task UpdateAsync(string id, T item); - - /// - /// Deletes item by Id - /// - /// - /// bool - bool Delete(string id); - - /// - /// Asynchronously deletes item by id - /// - /// - /// bool - /// - Task DeleteAsync(string id); - - } -} \ No newline at end of file diff --git a/Server/Services/Storage.cs b/Server/Services/Storage.cs deleted file mode 100644 index 733b5d7..0000000 --- a/Server/Services/Storage.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Server.Exceptions.Register; -using Server.Services.Interfaces; - -namespace Server.Services -{ - public class Storage : IStorage where T: class - { - private readonly IDeserializedObject _deserializedObject; //todo: change into something else. - - public Storage( - IDeserializedObject deserializedObject) - { - _deserializedObject = deserializedObject; - } - - /// - /// Gets all elements from T collection - /// - /// ICollection - - public ICollection GetAll() - { - return _deserializedObject.ConcurrentDictionary.Values; - } - - /// - /// Gets asynchronously all elements from T collection - /// - /// Task of ICollection - public async Task> GetAllAsync() - { - var result = Task.Run(GetAll); - return await result; - } - - /// - /// Gets element by Id - /// - /// Id of an element - /// T item - public T Get(string id) - { - return _deserializedObject.ConcurrentDictionary.TryGetValue(id, out var account) ? account : default; - } - - /// - /// Gets asynchronously element by Id - /// - /// string If of an Element - /// Task T item - public async Task GetAsync(string id) - { - var task = Task.Run(() => Get(id)); - return await task; - } - - /// - /// Adds T element to the collection - /// - /// T item - /// int - /// - /// - public int Add(T item) - { - var guid = GetGuidFromT(item); - if (typeof(T).Name.Contains("Round")) - { - if (!_deserializedObject.ConcurrentDictionary.TryAdd(guid.ToString(), item)) throw new UnknownReasonException(item.GetType().ToString()); - _deserializedObject.UpdateData(); - return (int)HttpStatusCode.OK; - } - - if (CheckIfExists(item)) - throw new AlreadyExistsException(item.GetType().ToString()); - - if (!_deserializedObject.ConcurrentDictionary.TryAdd(guid.ToString(), item)) throw new UnknownReasonException(item.GetType().ToString()); - _deserializedObject.UpdateData(); - return (int)HttpStatusCode.OK; - } - - /// - /// Adds asynchronously an item to ConcurrentDictionary - /// - /// T item - /// int - public Task AddAsync(T item) - { - return Task.Factory.StartNew(() => Add(item)); - } - - /// - /// Adds or updates item in collection - /// - /// id of item - /// T item - public void AddOrUpdate(string id, T item) - { - _deserializedObject.ConcurrentDictionary[id] = item; - } - /// - /// Asynchronously adds or updates item in collection - /// - /// id of item - /// T item - public Task AddOrUpdateAsync(string id, T item) - { - throw new NotImplementedException(); - } - - /// - /// Updates value in collection - /// - /// - /// - /// Task - public Task UpdateAsync(string id, T item) - { - return Task.Factory.StartNew(() => - { - var thisItem = _deserializedObject.ConcurrentDictionary[id]; - _deserializedObject.ConcurrentDictionary.TryUpdate(id, item, thisItem); - _deserializedObject.UpdateData(); - }); - } - - /// - /// Deletes item by Id - /// - /// - /// bool - public bool Delete(string id) - { - return _deserializedObject.ConcurrentDictionary.TryRemove(id, out _); - } - - /// - /// Asynchronously deletes item by id - /// - /// - /// bool - /// - public Task DeleteAsync(string id) - { - throw new NotImplementedException(); - } - - #region PrivateMethods - - /// - /// Gets Guid from T item - /// - /// - /// object string - private object GetGuidFromT(T item) - { - //METHOD TO GET GUID FROM T - //*************************** - var t = item.GetType(); - var prop = t.GetProperty("Id"); - return prop?.GetValue(item); - // ************************************* - } - - /// - /// Gets login property from T item - /// - /// - /// object string - private object GetLoginString(T item) - { - return item.GetType().GetProperty("Login") != null ? item.GetType().GetProperty("Login")?.GetValue(item) : null; - } - - /// - /// Checks if T item exists in collection - /// - /// - /// bool - private bool CheckIfExists(T item) - { - var flattenList = _deserializedObject.ConcurrentDictionary.Values; //THIS IS NOT ASYNC - return GetLoginString(item) != null && flattenList.Any(T => GetLoginString(T).Equals(GetLoginString(item))); - } - - #endregion - } -} \ No newline at end of file diff --git a/Server/Startup.cs b/Server/Startup.cs deleted file mode 100644 index 0194853..0000000 --- a/Server/Startup.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; -using Server.GameLogic.LogicServices; -using Server.GameLogic.LogicServices.Impl; -using Server.Services; -using Server.Services.Interfaces; - -namespace Server -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - private IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - - services.AddSingleton(typeof(IDeserializedObject<>), typeof(DeserializedObject<>)); - services.AddTransient(typeof(IStorage<>), typeof(Storage<>)); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddControllers(); - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo {Title = "Server", Version = "v1"}); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Server v1")); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.Map("/status/{sessionId}", async context => - { - var service = context.RequestServices.GetRequiredService(); //todo: remove - - var sessionId = (string) context.Request.RouteValues["sessionId"]; - - if (sessionId == null) - { - context.Response.StatusCode = (int) HttpStatusCode.BadRequest; - } - else if (await service.IsActive(sessionId)) - { - context.Response.StatusCode = (int) HttpStatusCode.OK; - } - else - { - context.Response.StatusCode = (int) HttpStatusCode.Forbidden; - } - }); - - endpoints.Map("/status/", async context => - { - context.Response.StatusCode = (int) HttpStatusCode.OK; - await context.Response.WriteAsync("alive"); - }); - - endpoints.MapControllers(); - - }); - } - } -} \ No newline at end of file diff --git a/Server/Statistics b/Server/Statistics deleted file mode 100644 index 4f3b58f..0000000 --- a/Server/Statistics +++ /dev/null @@ -1 +0,0 @@ -{"d3441061-4c3d-43ca-8aef-b733525ecd22":{"Id":"d3441061-4c3d-43ca-8aef-b733525ecd22","Login":"123","Wins":0,"Loss":0,"Draws":0,"WinLossRatio":0.0,"TimeSpent":null,"UsedRock":0,"UsedPaper":0,"UsedScissors":0,"Score":0}} \ No newline at end of file diff --git a/Server/appsettings.Development.json b/Server/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/Server/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/Server/appsettings.json b/Server/appsettings.json deleted file mode 100644 index d9d9a9b..0000000 --- a/Server/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -}