diff --git a/.editorconfig b/.editorconfig index 19d08bc..3d9637a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -91,4 +91,13 @@ dotnet_diagnostic.SA1500.severity = error csharp_style_namespace_declarations = file_scoped:warning # Disable XML docs -dotnet_diagnostic.CS1591.severity = none \ No newline at end of file +dotnet_diagnostic.CS1591.severity = none + +dotnet_diagnostic.CA1062.severity = none +dotnet_diagnostic.CA1034.severity = none + + +dotnet_diagnostic.CA2012.severity = none + +[/src/Lab5/ATM.Infrastructure.DataAccess/Migrations/*.cs] +dotnet_diagnostic.SA1649.severity = none diff --git a/Directory.Build.props b/Directory.Build.props index 53408c2..8543b2c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,6 +3,7 @@ net7.0 disable enable + true diff --git a/Directory.Packages.props b/Directory.Packages.props index dff3479..327678f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,12 +5,16 @@ + + + + diff --git a/Itmo.ObjectOrientedProgramming.sln b/Itmo.ObjectOrientedProgramming.sln index f7e1daa..d2f9075 100644 --- a/Itmo.ObjectOrientedProgramming.sln +++ b/Itmo.ObjectOrientedProgramming.sln @@ -21,8 +21,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab3", "src\Lab3\Lab3.cspro EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab4", "src\Lab4\Lab4.csproj", "{A53D3DBD-FA2D-47B4-90A9-B44AF07ADE71}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lab5", "Lab5", "{55C309C9-B7DE-4F64-AFDE-A33F9C2AB2D4}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab2.Tests", "tests\Lab2.Tests\Lab2.Tests.csproj", "{25D0E1F0-760B-4E76-B0D9-3EF274B35AF6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab3.Tests", "tests\Lab3.Tests\Lab3.Tests.csproj", "{7E5E46DB-EF55-47E6-822A-6AE20BF2FCD9}" @@ -31,6 +29,34 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab4.Tests", "tests\Lab4.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab5.Tests", "tests\Lab5.Tests\Lab5.Tests.csproj", "{FA627A62-AD7A-4A72-B15C-EC8AEFD206DB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lab5", "Lab5", "{99CB5FCA-AC39-4AD0-B4FF-A8C051CC2809}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentation", "Presentation", "{08E62A91-F7B4-4FE8-A1F8-BFD64F7E3A9E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{D6F0AF28-4B74-4CEF-BAE0-631C60979762}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{A882AA78-975B-4BF0-9193-5A0C5365974C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutomatedTellerMachine", "src\Lab5\AutomatedTellerMachine\AutomatedTellerMachine.csproj", "{9BAA271C-E0CC-42C8-83E7-FC398D53874F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATM.Application", "src\Lab5\ATM.Application\ATM.Application.csproj", "{6A71BA8C-6A36-4B80-9E97-4F99C8BFF1E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATM.Application.Models", "src\Lab5\ATM.Application.Models\ATM.Application.Models.csproj", "{8469CC97-19D1-4ABA-9213-99F03DA8BBF2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATM.Application.Abstractions", "src\Lab5\ATM.Application.Abstractions\ATM.Application.Abstractions.csproj", "{249A7B78-E81D-4127-901F-207159D59BCB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATM.Application.Contracts", "src\Lab5\ATM.Application.Contracts\ATM.Application.Contracts.csproj", "{D1EA8947-0596-48FF-A5AF-B6B51C63A484}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATM.Infrastructure.DataAccess", "src\Lab5\ATM.Infrastructure.DataAccess\ATM.Infrastructure.DataAccess.csproj", "{9B57FEE1-2A6D-49D8-8C48-AA3993BD1F82}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{E078567B-7E99-44F0-BE59-861B99056106}" + ProjectSection(SolutionItems) = preProject + src\Lab5\.env = src\Lab5\.env + src\Lab5\docker-compose.yaml = src\Lab5\docker-compose.yaml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATM.Presentation.Console", "src\Lab5\ATM.Presentation.Console\ATM.Presentation.Console.csproj", "{581A4324-47EC-4DF7-9839-F58450EA54F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,11 +68,22 @@ Global {433B9342-A6BF-4194-8F08-727AADAE7504} = {8ED774F4-D429-4B82-B768-50DCA8EFF609} {43B4CD48-18D5-4EDE-80CB-86460F4C64CB} = {8ED774F4-D429-4B82-B768-50DCA8EFF609} {A53D3DBD-FA2D-47B4-90A9-B44AF07ADE71} = {8ED774F4-D429-4B82-B768-50DCA8EFF609} - {55C309C9-B7DE-4F64-AFDE-A33F9C2AB2D4} = {8ED774F4-D429-4B82-B768-50DCA8EFF609} {25D0E1F0-760B-4E76-B0D9-3EF274B35AF6} = {792FFE57-F1A0-47A4-9E2A-80F75FF2F8DB} {7E5E46DB-EF55-47E6-822A-6AE20BF2FCD9} = {792FFE57-F1A0-47A4-9E2A-80F75FF2F8DB} {17689E4A-FE4F-4893-8E28-3BA911DEC8D8} = {792FFE57-F1A0-47A4-9E2A-80F75FF2F8DB} {FA627A62-AD7A-4A72-B15C-EC8AEFD206DB} = {792FFE57-F1A0-47A4-9E2A-80F75FF2F8DB} + {99CB5FCA-AC39-4AD0-B4FF-A8C051CC2809} = {8ED774F4-D429-4B82-B768-50DCA8EFF609} + {08E62A91-F7B4-4FE8-A1F8-BFD64F7E3A9E} = {99CB5FCA-AC39-4AD0-B4FF-A8C051CC2809} + {D6F0AF28-4B74-4CEF-BAE0-631C60979762} = {99CB5FCA-AC39-4AD0-B4FF-A8C051CC2809} + {A882AA78-975B-4BF0-9193-5A0C5365974C} = {99CB5FCA-AC39-4AD0-B4FF-A8C051CC2809} + {9BAA271C-E0CC-42C8-83E7-FC398D53874F} = {99CB5FCA-AC39-4AD0-B4FF-A8C051CC2809} + {6A71BA8C-6A36-4B80-9E97-4F99C8BFF1E9} = {D6F0AF28-4B74-4CEF-BAE0-631C60979762} + {8469CC97-19D1-4ABA-9213-99F03DA8BBF2} = {D6F0AF28-4B74-4CEF-BAE0-631C60979762} + {249A7B78-E81D-4127-901F-207159D59BCB} = {D6F0AF28-4B74-4CEF-BAE0-631C60979762} + {D1EA8947-0596-48FF-A5AF-B6B51C63A484} = {D6F0AF28-4B74-4CEF-BAE0-631C60979762} + {9B57FEE1-2A6D-49D8-8C48-AA3993BD1F82} = {A882AA78-975B-4BF0-9193-5A0C5365974C} + {E078567B-7E99-44F0-BE59-861B99056106} = {21E20419-0C74-46BC-9D7D-A24CEEC29ACC} + {581A4324-47EC-4DF7-9839-F58450EA54F0} = {08E62A91-F7B4-4FE8-A1F8-BFD64F7E3A9E} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {EABA4888-E6C1-4ACC-856E-FBD34CEA3EDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -85,5 +122,33 @@ Global {FA627A62-AD7A-4A72-B15C-EC8AEFD206DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA627A62-AD7A-4A72-B15C-EC8AEFD206DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA627A62-AD7A-4A72-B15C-EC8AEFD206DB}.Release|Any CPU.Build.0 = Release|Any CPU + {9BAA271C-E0CC-42C8-83E7-FC398D53874F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BAA271C-E0CC-42C8-83E7-FC398D53874F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BAA271C-E0CC-42C8-83E7-FC398D53874F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BAA271C-E0CC-42C8-83E7-FC398D53874F}.Release|Any CPU.Build.0 = Release|Any CPU + {6A71BA8C-6A36-4B80-9E97-4F99C8BFF1E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A71BA8C-6A36-4B80-9E97-4F99C8BFF1E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A71BA8C-6A36-4B80-9E97-4F99C8BFF1E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A71BA8C-6A36-4B80-9E97-4F99C8BFF1E9}.Release|Any CPU.Build.0 = Release|Any CPU + {8469CC97-19D1-4ABA-9213-99F03DA8BBF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8469CC97-19D1-4ABA-9213-99F03DA8BBF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8469CC97-19D1-4ABA-9213-99F03DA8BBF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8469CC97-19D1-4ABA-9213-99F03DA8BBF2}.Release|Any CPU.Build.0 = Release|Any CPU + {249A7B78-E81D-4127-901F-207159D59BCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {249A7B78-E81D-4127-901F-207159D59BCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {249A7B78-E81D-4127-901F-207159D59BCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {249A7B78-E81D-4127-901F-207159D59BCB}.Release|Any CPU.Build.0 = Release|Any CPU + {D1EA8947-0596-48FF-A5AF-B6B51C63A484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1EA8947-0596-48FF-A5AF-B6B51C63A484}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1EA8947-0596-48FF-A5AF-B6B51C63A484}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1EA8947-0596-48FF-A5AF-B6B51C63A484}.Release|Any CPU.Build.0 = Release|Any CPU + {9B57FEE1-2A6D-49D8-8C48-AA3993BD1F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B57FEE1-2A6D-49D8-8C48-AA3993BD1F82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B57FEE1-2A6D-49D8-8C48-AA3993BD1F82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B57FEE1-2A6D-49D8-8C48-AA3993BD1F82}.Release|Any CPU.Build.0 = Release|Any CPU + {581A4324-47EC-4DF7-9839-F58450EA54F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {581A4324-47EC-4DF7-9839-F58450EA54F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {581A4324-47EC-4DF7-9839-F58450EA54F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {581A4324-47EC-4DF7-9839-F58450EA54F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Lab5/.env b/src/Lab5/.env new file mode 100644 index 0000000..7dcf1da --- /dev/null +++ b/src/Lab5/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=atm_labwork \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Abstractions/ATM.Application.Abstractions.csproj b/src/Lab5/ATM.Application.Abstractions/ATM.Application.Abstractions.csproj new file mode 100644 index 0000000..871798a --- /dev/null +++ b/src/Lab5/ATM.Application.Abstractions/ATM.Application.Abstractions.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + AutomatedTellerMachine.Abstractions + + + + + + + diff --git a/src/Lab5/ATM.Application.Abstractions/Auth/GenerateResult.cs b/src/Lab5/ATM.Application.Abstractions/Auth/GenerateResult.cs new file mode 100644 index 0000000..9e0c62b --- /dev/null +++ b/src/Lab5/ATM.Application.Abstractions/Auth/GenerateResult.cs @@ -0,0 +1,10 @@ +namespace AutomatedTellerMachine.Abstractions.Auth; + +public abstract record GenerateResult +{ + private GenerateResult() { } + + public sealed record Success(string Token) : GenerateResult; + + public sealed record NotFound : GenerateResult; +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Abstractions/Auth/IAuthenticationService.cs b/src/Lab5/ATM.Application.Abstractions/Auth/IAuthenticationService.cs new file mode 100644 index 0000000..87580e0 --- /dev/null +++ b/src/Lab5/ATM.Application.Abstractions/Auth/IAuthenticationService.cs @@ -0,0 +1,7 @@ +namespace AutomatedTellerMachine.Abstractions.Auth; + +public interface IAuthenticationService +{ + GenerateResult Generate(long id, string hashedPin); + ValidationResult Validate(long id, string token); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Abstractions/Auth/ValidationResult.cs b/src/Lab5/ATM.Application.Abstractions/Auth/ValidationResult.cs new file mode 100644 index 0000000..10db1da --- /dev/null +++ b/src/Lab5/ATM.Application.Abstractions/Auth/ValidationResult.cs @@ -0,0 +1,12 @@ +namespace AutomatedTellerMachine.Abstractions.Auth; + +public abstract record ValidationResult +{ + private ValidationResult() { } + + public sealed record Success : ValidationResult; + + public sealed record TimedOut : ValidationResult; + + public sealed record NotFound : ValidationResult; +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Abstractions/Repositories/IAccountRepository.cs b/src/Lab5/ATM.Application.Abstractions/Repositories/IAccountRepository.cs new file mode 100644 index 0000000..6e4a43d --- /dev/null +++ b/src/Lab5/ATM.Application.Abstractions/Repositories/IAccountRepository.cs @@ -0,0 +1,12 @@ +using AutomatedTellerMachine.Models; + +namespace AutomatedTellerMachine.Abstractions.Repositories; + +public interface IAccountRepository +{ + Account CreateAccount(string hashedPin); + double GetBalanceByAccountId(long id); + + void Debit(long id, double amount); + void Credit(long id, double amount); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Abstractions/Repositories/IOperationsRepository.cs b/src/Lab5/ATM.Application.Abstractions/Repositories/IOperationsRepository.cs new file mode 100644 index 0000000..8a6476a --- /dev/null +++ b/src/Lab5/ATM.Application.Abstractions/Repositories/IOperationsRepository.cs @@ -0,0 +1,9 @@ +using AutomatedTellerMachine.Models; + +namespace AutomatedTellerMachine.Abstractions.Repositories; + +public interface IOperationsRepository +{ + IEnumerable GetOperationsByAccount(long id); + void AddOperation(long id, double amount); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Abstractions/Repositories/IPinRepository.cs b/src/Lab5/ATM.Application.Abstractions/Repositories/IPinRepository.cs new file mode 100644 index 0000000..5625128 --- /dev/null +++ b/src/Lab5/ATM.Application.Abstractions/Repositories/IPinRepository.cs @@ -0,0 +1,6 @@ +namespace AutomatedTellerMachine.Abstractions.Repositories; + +public interface IPinRepository +{ + string GetPinHashByAccountId(long id); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Contracts/ATM.Application.Contracts.csproj b/src/Lab5/ATM.Application.Contracts/ATM.Application.Contracts.csproj new file mode 100644 index 0000000..9acd01f --- /dev/null +++ b/src/Lab5/ATM.Application.Contracts/ATM.Application.Contracts.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + AutomatedTellerMachine.Contracts + + + + + + + diff --git a/src/Lab5/ATM.Application.Contracts/Accounts/IAccountService.cs b/src/Lab5/ATM.Application.Contracts/Accounts/IAccountService.cs new file mode 100644 index 0000000..32a227c --- /dev/null +++ b/src/Lab5/ATM.Application.Contracts/Accounts/IAccountService.cs @@ -0,0 +1,16 @@ +using AutomatedTellerMachine.Models; + +namespace AutomatedTellerMachine.Contracts.Accounts; + +public interface IAccountService +{ + LoginResult Login(long id, string hashedPin); + + double GetBalance(long id, string token); + + OperationResult Credit(double amount, long id, string token); + OperationResult Debit(double amount, long id, string token); + + IEnumerable GetOperations(long id, string token); + long Create(string hashedPin); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Contracts/Accounts/LoginResult.cs b/src/Lab5/ATM.Application.Contracts/Accounts/LoginResult.cs new file mode 100644 index 0000000..64c7a71 --- /dev/null +++ b/src/Lab5/ATM.Application.Contracts/Accounts/LoginResult.cs @@ -0,0 +1,10 @@ +namespace AutomatedTellerMachine.Contracts.Accounts; + +public abstract record LoginResult +{ + private LoginResult() { } + + public sealed record Success(string Token) : LoginResult; + + public sealed record NotFound : LoginResult; +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Contracts/Accounts/OperationResult.cs b/src/Lab5/ATM.Application.Contracts/Accounts/OperationResult.cs new file mode 100644 index 0000000..4d3b5ec --- /dev/null +++ b/src/Lab5/ATM.Application.Contracts/Accounts/OperationResult.cs @@ -0,0 +1,10 @@ +namespace AutomatedTellerMachine.Contracts.Accounts; + +public abstract record OperationResult +{ + private OperationResult() { } + + public sealed record Success : OperationResult; + + public sealed record Failure : OperationResult; +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Contracts/Admin/IAdminService.cs b/src/Lab5/ATM.Application.Contracts/Admin/IAdminService.cs new file mode 100644 index 0000000..06f2b41 --- /dev/null +++ b/src/Lab5/ATM.Application.Contracts/Admin/IAdminService.cs @@ -0,0 +1,8 @@ +using AutomatedTellerMachine.Contracts.Accounts; + +namespace AutomatedTellerMachine.Contracts.Admin; + +public interface IAdminService +{ + LoginResult AdminLogin(string hashPass); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Models/ATM.Application.Models.csproj b/src/Lab5/ATM.Application.Models/ATM.Application.Models.csproj new file mode 100644 index 0000000..5cace48 --- /dev/null +++ b/src/Lab5/ATM.Application.Models/ATM.Application.Models.csproj @@ -0,0 +1,10 @@ + + + + net7.0 + enable + enable + AutomatedTellerMachine.Models + + + diff --git a/src/Lab5/ATM.Application.Models/Account.cs b/src/Lab5/ATM.Application.Models/Account.cs new file mode 100644 index 0000000..b4f9f71 --- /dev/null +++ b/src/Lab5/ATM.Application.Models/Account.cs @@ -0,0 +1,3 @@ +namespace AutomatedTellerMachine.Models; + +public record Account(long Id); \ No newline at end of file diff --git a/src/Lab5/ATM.Application.Models/Operation.cs b/src/Lab5/ATM.Application.Models/Operation.cs new file mode 100644 index 0000000..78ed25e --- /dev/null +++ b/src/Lab5/ATM.Application.Models/Operation.cs @@ -0,0 +1,3 @@ +namespace AutomatedTellerMachine.Models; + +public record Operation(long Id, double Amount); \ No newline at end of file diff --git a/src/Lab5/ATM.Application/ATM.Application.csproj b/src/Lab5/ATM.Application/ATM.Application.csproj new file mode 100644 index 0000000..8a9d583 --- /dev/null +++ b/src/Lab5/ATM.Application/ATM.Application.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + AutomatedTellerMachine.Application + + + + + + + + + + + + diff --git a/src/Lab5/ATM.Application/Accounts/AccountService.cs b/src/Lab5/ATM.Application/Accounts/AccountService.cs new file mode 100644 index 0000000..e6344fc --- /dev/null +++ b/src/Lab5/ATM.Application/Accounts/AccountService.cs @@ -0,0 +1,74 @@ +using AutomatedTellerMachine.Abstractions.Auth; +using AutomatedTellerMachine.Abstractions.Repositories; +using AutomatedTellerMachine.Contracts.Accounts; +using AutomatedTellerMachine.Models; + +namespace AutomatedTellerMachine.Application.Accounts; + +public class AccountService : IAccountService +{ + private readonly IAuthenticationService _authenticationService; + private readonly IAccountRepository _accountRepository; + private readonly IOperationsRepository _operationsRepository; + + public AccountService(IAuthenticationService authenticationService, IAccountRepository accountRepository, IOperationsRepository operationsRepository) + { + _authenticationService = authenticationService; + _accountRepository = accountRepository; + _operationsRepository = operationsRepository; + } + + public LoginResult Login(long id, string hashedPin) + { + GenerateResult token = _authenticationService.Generate(id, hashedPin); + + return token switch + { + GenerateResult.Success success => new LoginResult.Success(success.Token), + _ => new LoginResult.NotFound(), + }; + } + + public double GetBalance(long id, string token) + { + _authenticationService.Validate(id, token); + + return _accountRepository.GetBalanceByAccountId(id); + } + + public IEnumerable GetOperations(long id, string token) + { + _authenticationService.Validate(id, token); + + return _operationsRepository.GetOperationsByAccount(id); + } + + public OperationResult Credit(double amount, long id, string token) + { + _authenticationService.Validate(id, token); + + double balance = _accountRepository.GetBalanceByAccountId(id); + + if (balance < amount) return new OperationResult.Failure(); + + _operationsRepository.AddOperation(id, -amount); + _accountRepository.Credit(id, amount); + return new OperationResult.Success(); + } + + public OperationResult Debit(double amount, long id, string token) + { + _authenticationService.Validate(id, token); + + _operationsRepository.AddOperation(id, amount); + + _accountRepository.Debit(id, amount); + Console.WriteLine(amount); + return new OperationResult.Success(); + } + + public long Create(string hashedPin) + { + return _accountRepository.CreateAccount(hashedPin).Id; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application/Admin/AdminService.cs b/src/Lab5/ATM.Application/Admin/AdminService.cs new file mode 100644 index 0000000..cf7edcc --- /dev/null +++ b/src/Lab5/ATM.Application/Admin/AdminService.cs @@ -0,0 +1,29 @@ +using AutomatedTellerMachine.Abstractions.Auth; +using AutomatedTellerMachine.Abstractions.Repositories; +using AutomatedTellerMachine.Contracts.Accounts; +using AutomatedTellerMachine.Contracts.Admin; + +namespace AutomatedTellerMachine.Application.Admin; + +public class AdminService : IAdminService +{ + private readonly IAccountRepository _accountRepository; + private readonly IAuthenticationService _authenticationService; + + public AdminService(IAccountRepository accountRepository, IAuthenticationService authenticationService) + { + _accountRepository = accountRepository; + _authenticationService = authenticationService; + } + + public LoginResult AdminLogin(string hashPass) + { + GenerateResult token = _authenticationService.Generate(0, hashPass); + + return token switch + { + GenerateResult.Success success => new LoginResult.Success(success.Token), + _ => new LoginResult.NotFound(), + }; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application/Auth/AuthenticationService.cs b/src/Lab5/ATM.Application/Auth/AuthenticationService.cs new file mode 100644 index 0000000..7ef1bb0 --- /dev/null +++ b/src/Lab5/ATM.Application/Auth/AuthenticationService.cs @@ -0,0 +1,45 @@ +using AutomatedTellerMachine.Abstractions.Auth; +using AutomatedTellerMachine.Abstractions.Repositories; + +namespace AutomatedTellerMachine.Application.Auth; + +public class AuthenticationService : IAuthenticationService +{ + private readonly Dictionary _tokens = new(); + private readonly IPinRepository _pinRepository; + + public AuthenticationService(IPinRepository pinRepository) + { + _pinRepository = pinRepository; + } + + public GenerateResult Generate(long id, string hashedPin) + { + string localPinHash = _pinRepository.GetPinHashByAccountId(id); + + return localPinHash == hashedPin ? new GenerateResult.Success(GenerateToken(id)) : new GenerateResult.NotFound(); + } + + public ValidationResult Validate(long id, string token) + { + if (_tokens[token] != id) return new ValidationResult.NotFound(); + + byte[] data = Convert.FromBase64String(token); + var when = DateTime.FromBinary(BitConverter.ToInt64(data, 0)); + if (when < DateTime.UtcNow.AddMinutes(-5)) + { + return new ValidationResult.TimedOut(); + } + + return new ValidationResult.Success(); + } + + private string GenerateToken(long id) + { + byte[] time = BitConverter.GetBytes(DateTime.UtcNow.ToBinary()); + byte[] key = Guid.NewGuid().ToByteArray(); + string token = Convert.ToBase64String(time.Concat(key).ToArray()); + _tokens.Add(token, id); + return token; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Application/Extensions/ServiceCollectionExtensions.cs b/src/Lab5/ATM.Application/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..009a247 --- /dev/null +++ b/src/Lab5/ATM.Application/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using AutomatedTellerMachine.Abstractions.Auth; +using AutomatedTellerMachine.Application.Accounts; +using AutomatedTellerMachine.Application.Auth; +using AutomatedTellerMachine.Contracts.Accounts; +using Microsoft.Extensions.DependencyInjection; + +namespace AutomatedTellerMachine.Application.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddApplication(this IServiceCollection collection) + { + collection.AddScoped(); + collection.AddScoped(); + + return collection; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Infrastructure.DataAccess/ATM.Infrastructure.DataAccess.csproj b/src/Lab5/ATM.Infrastructure.DataAccess/ATM.Infrastructure.DataAccess.csproj new file mode 100644 index 0000000..a4cbf8e --- /dev/null +++ b/src/Lab5/ATM.Infrastructure.DataAccess/ATM.Infrastructure.DataAccess.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/src/Lab5/ATM.Infrastructure.DataAccess/Extensions/ServiceCollectionExtensions.cs b/src/Lab5/ATM.Infrastructure.DataAccess/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9a2dd4f --- /dev/null +++ b/src/Lab5/ATM.Infrastructure.DataAccess/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using ATM.Infrastructure.DataAccess.Repositories; +using AutomatedTellerMachine.Abstractions.Repositories; +using Itmo.Dev.Platform.Postgres.Extensions; +using Itmo.Dev.Platform.Postgres.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace ATM.Infrastructure.DataAccess.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddInfrastructureDataAccess( + this IServiceCollection collection, + Action configuration) + { + collection.AddPlatformPostgres(builder => builder.Configure(configuration)); + collection.AddPlatformMigrations(typeof(ServiceCollectionExtensions).Assembly); + + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + + return collection; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Infrastructure.DataAccess/Extensions/ServiceScopeExtensions.cs b/src/Lab5/ATM.Infrastructure.DataAccess/Extensions/ServiceScopeExtensions.cs new file mode 100644 index 0000000..696bbbd --- /dev/null +++ b/src/Lab5/ATM.Infrastructure.DataAccess/Extensions/ServiceScopeExtensions.cs @@ -0,0 +1,12 @@ +using Itmo.Dev.Platform.Postgres.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace ATM.Infrastructure.DataAccess.Extensions; + +public static class ServiceScopeExtensions +{ + public static void UseInfrastructureDataAccess(this IServiceScope scope) + { + scope.UsePlatformMigrationsAsync(default).GetAwaiter().GetResult(); + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Infrastructure.DataAccess/Migrations/01_Initial.cs b/src/Lab5/ATM.Infrastructure.DataAccess/Migrations/01_Initial.cs new file mode 100644 index 0000000..b1797ea --- /dev/null +++ b/src/Lab5/ATM.Infrastructure.DataAccess/Migrations/01_Initial.cs @@ -0,0 +1,36 @@ +using FluentMigrator; +using Itmo.Dev.Platform.Postgres.Migrations; + +namespace ATM.Infrastructure.DataAccess.Migrations; + +[Migration(1, "Initial")] +public class Initial : SqlMigration +{ +protected override string GetUpSql(IServiceProvider serviceProvider) => + """ + create table passwords + ( + account_id bigint primary key , + hash char(64) not null + ); + + create table accounts + ( + account_id bigint primary key generated always as identity, + balance bigint not null + ); + + create table operations + ( + account_id bigint primary key, + amount double precision not null + ); + """; + +protected override string GetDownSql(IServiceProvider serviceProvider) => + """ + drop table accounts; + drop table operations; + drop table passwords; + """; +} \ No newline at end of file diff --git a/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/AccountRepository.cs b/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/AccountRepository.cs new file mode 100644 index 0000000..4a137d5 --- /dev/null +++ b/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/AccountRepository.cs @@ -0,0 +1,113 @@ +using AutomatedTellerMachine.Abstractions.Repositories; +using AutomatedTellerMachine.Models; +using Itmo.Dev.Platform.Postgres.Connection; +using Npgsql; + +namespace ATM.Infrastructure.DataAccess.Repositories; + +public class AccountRepository : IAccountRepository +{ + private readonly IPostgresConnectionProvider _connectionProvider; + + public AccountRepository(IPostgresConnectionProvider connectionProvider) + { + _connectionProvider = connectionProvider; + } + + public double GetBalanceByAccountId(long id) + { + const string sql = @" + SELECT balance FROM accounts WHERE account_id = @AccountId"; + + NpgsqlConnection connection = _connectionProvider + .GetConnectionAsync(default) + .GetAwaiter() + .GetResult(); + + using var command = new NpgsqlCommand(sql, connection); + command.Parameters.AddWithValue("@AccountId", id); + + object? result = command.ExecuteScalar(); + + return result != null ? (double)result : 0; + } + + public void Debit(long id, double amount) + { + const string sql = @" + UPDATE accounts SET balance = balance + @Amount WHERE account_id = @AccountId"; + + NpgsqlConnection connection = _connectionProvider + .GetConnectionAsync(default) + .GetAwaiter() + .GetResult(); + + using var command = new NpgsqlCommand(sql, connection); + command.Parameters.AddWithValue("@AccountId", id); + command.Parameters.AddWithValue("@Amount", amount); + + command.ExecuteScalar(); + } + + public void Credit(long id, double amount) + { + const string sql = @" + UPDATE accounts SET balance = balance - @Amount WHERE account_id = @AccountId"; + + NpgsqlConnection connection = _connectionProvider + .GetConnectionAsync(default) + .GetAwaiter() + .GetResult(); + + using var command = new NpgsqlCommand(sql, connection); + command.Parameters.AddWithValue("@AccountId", id); + command.Parameters.AddWithValue("@Amount", amount); + + command.ExecuteScalar(); + } + + public Account CreateAccount(string hashedPin) + { + const string accountSql = @" + INSERT INTO accounts (balance) + VALUES (@Balance) + RETURNING account_id;"; + + const string passwordSql = @" + INSERT INTO passwords (account_id, hash) + VALUES (@AccountId, @Hash);"; + + NpgsqlConnection connection = _connectionProvider + .GetConnectionAsync(default) + .GetAwaiter() + .GetResult(); + + using NpgsqlTransaction transaction = connection.BeginTransaction(); + + try + { + // Insert into accounts table + using (var accountCommand = new NpgsqlCommand(accountSql, connection, transaction)) + { + accountCommand.Parameters.AddWithValue("@Balance", 0); + long accountId = (long)(accountCommand.ExecuteScalar() ?? 0); + + // Insert into passwords table + using (var passwordCommand = new NpgsqlCommand(passwordSql, connection, transaction)) + { + passwordCommand.Parameters.AddWithValue("@AccountId", accountId); + passwordCommand.Parameters.AddWithValue("@Hash", hashedPin); + passwordCommand.ExecuteNonQuery(); + } + + transaction.Commit(); + return new Account(accountId); + } + } + catch (Exception) + { + transaction.Rollback(); + throw; // Re-throw the exception to be handled by the caller + } + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/OperationsRepository.cs b/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/OperationsRepository.cs new file mode 100644 index 0000000..c3ea911 --- /dev/null +++ b/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/OperationsRepository.cs @@ -0,0 +1,64 @@ +using AutomatedTellerMachine.Abstractions.Repositories; +using AutomatedTellerMachine.Models; +using Itmo.Dev.Platform.Postgres.Connection; +using Npgsql; + +namespace ATM.Infrastructure.DataAccess.Repositories; + +public class OperationsRepository : IOperationsRepository +{ + private readonly IPostgresConnectionProvider _connectionProvider; + + public OperationsRepository(IPostgresConnectionProvider connectionProvider) + { + _connectionProvider = connectionProvider; + } + + public IEnumerable GetOperationsByAccount(long id) + { + const string sql = @"SELECT * FROM operations WHERE account_id = @AccountId"; + + NpgsqlConnection connection = _connectionProvider + .GetConnectionAsync(default) + .GetAwaiter() + .GetResult(); + + var operations = new List(); + + using (var command = new NpgsqlCommand(sql, connection)) + { + command.Parameters.AddWithValue("@AccountId", id); + + using (NpgsqlDataReader reader = command.ExecuteReader()) + { + while (reader.Read()) + { + // Assuming 'Operation' has properties corresponding to your table columns + var operation = new Operation((long)reader["account_id"], (double)reader["amount"]); + + operations.Add(operation); + } + } + } + + return operations; + } + + public void AddOperation(long id, double amount) + { + const string sql = @"INSERT INTO operations (account_id, amount) + VALUES (@AccountID, @Amount) + "; + + NpgsqlConnection connection = _connectionProvider + .GetConnectionAsync(default) + .GetAwaiter() + .GetResult(); + + using var command = new NpgsqlCommand(sql, connection); + command.Parameters.AddWithValue("@AccountId", id); + command.Parameters.AddWithValue("@Amount", amount); + + command.ExecuteScalar(); + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/PinRepository.cs b/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/PinRepository.cs new file mode 100644 index 0000000..533a4be --- /dev/null +++ b/src/Lab5/ATM.Infrastructure.DataAccess/Repositories/PinRepository.cs @@ -0,0 +1,34 @@ +using AutomatedTellerMachine.Abstractions.Repositories; +using Itmo.Dev.Platform.Postgres.Connection; +using Npgsql; + +namespace ATM.Infrastructure.DataAccess.Repositories; + +public class PinRepository : IPinRepository +{ + private readonly IPostgresConnectionProvider _connectionProvider; + public PinRepository(IPostgresConnectionProvider connectionProvider) + { + _connectionProvider = connectionProvider; + } + + public string GetPinHashByAccountId(long id) + { + const string sql = @" + SELECT hash + FROM passwords + WHERE account_id = @UserId;"; + + NpgsqlConnection connection = _connectionProvider + .GetConnectionAsync(default) + .GetAwaiter() + .GetResult(); + + using var command = new NpgsqlCommand(sql, connection); + command.Parameters.AddWithValue("@UserId", id); + + object? result = command.ExecuteScalar(); + + return result != null ? ((string)result).Trim() : string.Empty; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/ATM.Presentation.Console.csproj b/src/Lab5/ATM.Presentation.Console/ATM.Presentation.Console.csproj new file mode 100644 index 0000000..07ff5e0 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/ATM.Presentation.Console.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/src/Lab5/ATM.Presentation.Console/CurrentAccount/CurrentAccount.cs b/src/Lab5/ATM.Presentation.Console/CurrentAccount/CurrentAccount.cs new file mode 100644 index 0000000..28ef7ee --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/CurrentAccount/CurrentAccount.cs @@ -0,0 +1,5 @@ +using AutomatedTellerMachine.Models; + +namespace ATM.Presentation.Console.CurrentAccount; + +public record CurrentAccount(long Id, string Token) : Account(Id); \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/CurrentAccount/CurrentAccountService.cs b/src/Lab5/ATM.Presentation.Console/CurrentAccount/CurrentAccountService.cs new file mode 100644 index 0000000..c59b70f --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/CurrentAccount/CurrentAccountService.cs @@ -0,0 +1,6 @@ +namespace ATM.Presentation.Console.CurrentAccount; + +public class CurrentAccountService +{ + public CurrentAccount? CurrentAccount { get; set; } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Extensions/ServiceCollectionExtensions.cs b/src/Lab5/ATM.Presentation.Console/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..253f9fa --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using ATM.Presentation.Console.CurrentAccount; +using ATM.Presentation.Console.Scenarios.Admin; +using ATM.Presentation.Console.Scenarios.Login; +using ATM.Presentation.Console.Scenarios.Menu; +using ATM.Presentation.Console.Scenarios.Register; +using Microsoft.Extensions.DependencyInjection; + +namespace ATM.Presentation.Console.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddPresentationConsole(this IServiceCollection collection) + { + collection.AddScoped(); + + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + + return collection; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/IScenario.cs b/src/Lab5/ATM.Presentation.Console/IScenario.cs new file mode 100644 index 0000000..d2d2deb --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/IScenario.cs @@ -0,0 +1,10 @@ +namespace ATM.Presentation.Console; + +public interface IScenario +{ + string Name { get; } + + void Run(); + + void SetNext(IScenario scenario); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/IScenarioProvider.cs b/src/Lab5/ATM.Presentation.Console/IScenarioProvider.cs new file mode 100644 index 0000000..d2fb25c --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/IScenarioProvider.cs @@ -0,0 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ATM.Presentation.Console; + +public interface IScenarioProvider +{ + bool TryGetScenario([NotNullWhen(true)] out IScenario? scenario); +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/ScenarioRunner.cs b/src/Lab5/ATM.Presentation.Console/ScenarioRunner.cs new file mode 100644 index 0000000..45fc204 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/ScenarioRunner.cs @@ -0,0 +1,35 @@ +using Spectre.Console; + +namespace ATM.Presentation.Console; + +public class ScenarioRunner +{ + private readonly IEnumerable _providers; + + public ScenarioRunner(IEnumerable providers) + { + _providers = providers; + } + + public void Run() + { + IEnumerable scenarios = GetScenarios(); + + SelectionPrompt selector = new SelectionPrompt() + .Title("Select action") + .AddChoices(scenarios) + .UseConverter(x => x.Name); + + IScenario scenario = AnsiConsole.Prompt(selector); + scenario.Run(); + } + + private IEnumerable GetScenarios() + { + foreach (IScenarioProvider provider in _providers) + { + if (provider.TryGetScenario(out IScenario? scenario)) + yield return scenario; + } + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/AdminScenarioProvider.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/AdminScenarioProvider.cs new file mode 100644 index 0000000..7f6ec9d --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/AdminScenarioProvider.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using ATM.Presentation.Console.CurrentAccount; +using AutomatedTellerMachine.Contracts.Accounts; + +namespace ATM.Presentation.Console.Scenarios.Admin; + +public class AdminScenarioProvider : IScenarioProvider +{ + private readonly IAccountService _service; + private readonly CurrentAccountService _currentAccountService; + + public AdminScenarioProvider( + IAccountService service, CurrentAccountService currentAccountService) + { + _service = service; + _currentAccountService = currentAccountService; + } + + public bool TryGetScenario( + [NotNullWhen(true)] out IScenario? scenario) + { + if (_currentAccountService.CurrentAccount == null) + { + // scenario = new AdminScenario(_service, _currentAccountService.CurrentAccount); + // return true; + } + + scenario = null; + return false; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/Chain/AdminMenuScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/Chain/AdminMenuScenario.cs new file mode 100644 index 0000000..1f33a7f --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/Chain/AdminMenuScenario.cs @@ -0,0 +1,40 @@ +using System.Text; +using AutomatedTellerMachine.Contracts.Accounts; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Admin.Chain; + +public class AdminMenuScenario : IScenario +{ + private readonly List _scenarios = new(); + private readonly IAccountService _accountService; + private readonly CurrentAccount.CurrentAccount _current; + + public AdminMenuScenario(IAccountService accountService, CurrentAccount.CurrentAccount current) + { + _accountService = accountService; + _current = current; + } + + public string Name => "Menu"; + + public void Run() + { + var sb = new StringBuilder(); + sb.Append("Current Balance: "); + sb.Append(_accountService.GetBalance(_current.Id, _current.Token)); + + SelectionPrompt selector = new SelectionPrompt() + .Title(sb.ToString()) + .AddChoices(_scenarios) + .UseConverter(x => x.Name); + + IScenario scenario = AnsiConsole.Prompt(selector); + scenario.Run(); + } + + public void SetNext(IScenario scenario) + { + _scenarios.Add(scenario); + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/Chain/AdminScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/Chain/AdminScenario.cs new file mode 100644 index 0000000..0e93356 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Admin/Chain/AdminScenario.cs @@ -0,0 +1,39 @@ +using ATM.Presentation.Console.CurrentAccount; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Admin.Chain; + +public class AdminScenario : IScenario +{ + private readonly CurrentAccountService _currentAccountService; + + public AdminScenario(CurrentAccountService currentAccountService) + { + _currentAccountService = currentAccountService; + } + + public string Name => "Admin"; + + public void Run() + { + string password = AnsiConsole.Ask("Enter admin password: "); + + // string hashedPass = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(password))); + + // LoginResult result = _adminService.AdminLogin(hashedPass); + // switch (result) + // { + // case LoginResult.Success success: + // _currentAccountService.CurrentAccount = new CurrentAccount.CurrentAccount(0, success.Token); + // break; + // case LoginResult.NotFound: + // break; + // } + AnsiConsole.WriteLine(password); + } + + public void SetNext(IScenario scenario) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Login/LoginScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Login/LoginScenario.cs new file mode 100644 index 0000000..433d68b --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Login/LoginScenario.cs @@ -0,0 +1,49 @@ +using System.Security.Cryptography; +using System.Text; +using ATM.Presentation.Console.CurrentAccount; +using AutomatedTellerMachine.Contracts.Accounts; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Login; + +public class LoginScenario : IScenario +{ + private readonly IAccountService _accountService; + private readonly CurrentAccountService _currentAccountService; + + public LoginScenario(IAccountService accountService, CurrentAccountService currentAccountService) + { + _accountService = accountService; + _currentAccountService = currentAccountService; + } + + public string Name => "Login"; + + public void Run() + { + long account = AnsiConsole.Ask("Enter your account"); + string pin = AnsiConsole.Ask("Enter your pin"); + + string hashedPin = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(pin))); + + LoginResult result = _accountService.Login(account, hashedPin); + + switch (result) + { + case LoginResult.Success success: + AnsiConsole.WriteLine("Login successful."); + _currentAccountService.CurrentAccount = new CurrentAccount.CurrentAccount(account, success.Token); + break; + case LoginResult.NotFound: + AnsiConsole.WriteLine("Account not found or incorrect password."); + break; + default: + throw new ArgumentOutOfRangeException(nameof(result)); + } + } + + public void SetNext(IScenario scenario) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Login/LoginScenarioProvider.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Login/LoginScenarioProvider.cs new file mode 100644 index 0000000..20be725 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Login/LoginScenarioProvider.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using ATM.Presentation.Console.CurrentAccount; +using AutomatedTellerMachine.Contracts.Accounts; + +namespace ATM.Presentation.Console.Scenarios.Login; + +public class LoginScenarioProvider : IScenarioProvider +{ + private readonly IAccountService _service; + private readonly CurrentAccountService _currentAccountService; + + public LoginScenarioProvider( + IAccountService service, CurrentAccountService currentAccountService) + { + _service = service; + _currentAccountService = currentAccountService; + } + + public bool TryGetScenario( + [NotNullWhen(true)] out IScenario? scenario) + { + if (_currentAccountService.CurrentAccount == null) + { + scenario = new LoginScenario(_service, _currentAccountService); + return true; + } + + scenario = null; + return false; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/CreditScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/CreditScenario.cs new file mode 100644 index 0000000..3ba926f --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/CreditScenario.cs @@ -0,0 +1,34 @@ +using AutomatedTellerMachine.Contracts.Accounts; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Menu.Chain; + +public class CreditScenario : IScenario +{ + private readonly IAccountService _accountService; + private readonly CurrentAccount.CurrentAccount _current; + private IScenario? _nextScenario; + + public CreditScenario(IAccountService accountService, CurrentAccount.CurrentAccount current) + { + _accountService = accountService; + _current = current; + } + + public string Name => "Credit"; + + public void Run() + { + double amount = AnsiConsole.Ask("How much would you like to credit?\n"); + + _accountService.Credit(amount, _current.Id, _current.Token); + + AnsiConsole.Clear(); + _nextScenario?.Run(); + } + + public void SetNext(IScenario scenario) + { + _nextScenario = scenario; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/DebitScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/DebitScenario.cs new file mode 100644 index 0000000..b12b1c6 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/DebitScenario.cs @@ -0,0 +1,34 @@ +using AutomatedTellerMachine.Contracts.Accounts; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Menu.Chain; + +public class DebitScenario : IScenario +{ + private readonly IAccountService _accountService; + private readonly CurrentAccount.CurrentAccount _current; + private IScenario? _nextScenario; + + public DebitScenario(IAccountService accountService, CurrentAccount.CurrentAccount current) + { + _accountService = accountService; + _current = current; + } + + public string Name => "Debit"; + + public void Run() + { + double amount = AnsiConsole.Ask("How much would you like to credit?\n"); + + _accountService.Debit(amount, _current.Id, _current.Token); + + AnsiConsole.Clear(); + _nextScenario?.Run(); + } + + public void SetNext(IScenario scenario) + { + _nextScenario = scenario; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/LogoutScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/LogoutScenario.cs new file mode 100644 index 0000000..3dae929 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/LogoutScenario.cs @@ -0,0 +1,27 @@ +using ATM.Presentation.Console.CurrentAccount; + +namespace ATM.Presentation.Console.Scenarios.Menu.Chain; + +public class LogoutScenario : IScenario +{ + private readonly CurrentAccountService _currentAccountService; + private IScenario? _nextScenario; + + public LogoutScenario(CurrentAccountService currentAccountService) + { + _currentAccountService = currentAccountService; + } + + public string Name => "Logout"; + + public void Run() + { + _currentAccountService.CurrentAccount = null; + _nextScenario?.Run(); + } + + public void SetNext(IScenario scenario) + { + _nextScenario = scenario; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/MenuScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/MenuScenario.cs new file mode 100644 index 0000000..ed9798d --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/MenuScenario.cs @@ -0,0 +1,40 @@ +using System.Text; +using AutomatedTellerMachine.Contracts.Accounts; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Menu.Chain; + +public class MenuScenario : IScenario +{ + private readonly List _scenarios = new(); + private readonly IAccountService _accountService; + private readonly CurrentAccount.CurrentAccount _current; + + public MenuScenario(IAccountService accountService, CurrentAccount.CurrentAccount current) + { + _accountService = accountService; + _current = current; + } + + public string Name => "Menu"; + + public void Run() + { + var sb = new StringBuilder(); + sb.Append("Current Balance: "); + sb.Append(_accountService.GetBalance(_current.Id, _current.Token)); + + SelectionPrompt selector = new SelectionPrompt() + .Title(sb.ToString()) + .AddChoices(_scenarios) + .UseConverter(x => x.Name); + + IScenario scenario = AnsiConsole.Prompt(selector); + scenario.Run(); + } + + public void SetNext(IScenario scenario) + { + _scenarios.Add(scenario); + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/RecentOperationsScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/RecentOperationsScenario.cs new file mode 100644 index 0000000..1c06b57 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/Chain/RecentOperationsScenario.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Text; +using AutomatedTellerMachine.Contracts.Accounts; +using AutomatedTellerMachine.Models; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Menu.Chain; + +public class RecentOperationsScenario : IScenario +{ + private readonly IAccountService _accountService; + private readonly CurrentAccount.CurrentAccount _current; + private IScenario? _nextScenario; + + public RecentOperationsScenario(IAccountService accountService, CurrentAccount.CurrentAccount current) + { + _accountService = accountService; + _current = current; + } + + public string Name => "Recent Operations"; + + public void Run() + { + IEnumerable operations = _accountService.GetOperations(_current.Id, _current.Token); + + AnsiConsole.WriteLine("Recent Operations:"); + + var sb = new StringBuilder(); + + foreach (Operation operation in operations) + { + sb.AppendLine(operation.Amount.ToString(CultureInfo.CurrentCulture)); + } + + AnsiConsole.WriteLine(sb.ToString()); + + AnsiConsole.Console.Input.ReadKey(false); + AnsiConsole.Clear(); + _nextScenario?.Run(); + } + + public void SetNext(IScenario scenario) + { + _nextScenario = scenario; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/MenuScenarioProvider.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/MenuScenarioProvider.cs new file mode 100644 index 0000000..defe4f8 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Menu/MenuScenarioProvider.cs @@ -0,0 +1,55 @@ +using System.Diagnostics.CodeAnalysis; +using ATM.Presentation.Console.CurrentAccount; +using ATM.Presentation.Console.Scenarios.Menu.Chain; +using AutomatedTellerMachine.Contracts.Accounts; + +namespace ATM.Presentation.Console.Scenarios.Menu; + +public class MenuScenarioProvider : IScenarioProvider +{ + private readonly IAccountService _service; + private readonly CurrentAccountService _currentAccountService; + + public MenuScenarioProvider( + IAccountService service, CurrentAccountService currentAccountService) + { + _service = service; + _currentAccountService = currentAccountService; + } + + public bool TryGetScenario( + [NotNullWhen(true)] out IScenario? scenario) + { + if (_currentAccountService.CurrentAccount != null) + { + scenario = GetScenarioChain(_currentAccountService.CurrentAccount); + return true; + } + + scenario = null; + return false; + } + + private IScenario GetScenarioChain(CurrentAccount.CurrentAccount account) + { + var entry = new MenuScenario(_service, account); + + var credit = new CreditScenario(_service, account); + credit.SetNext(entry); + + var debit = new DebitScenario(_service, account); + debit.SetNext(entry); + + var operations = new RecentOperationsScenario(_service, account); + operations.SetNext(entry); + + var logout = new LogoutScenario(_currentAccountService); + + entry.SetNext(debit); + entry.SetNext(credit); + entry.SetNext(operations); + entry.SetNext(logout); + + return entry; + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Register/RegisterScenario.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Register/RegisterScenario.cs new file mode 100644 index 0000000..4eff994 --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Register/RegisterScenario.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using ATM.Presentation.Console.CurrentAccount; +using AutomatedTellerMachine.Contracts.Accounts; +using Spectre.Console; + +namespace ATM.Presentation.Console.Scenarios.Register; + +public class RegisterScenario : IScenario +{ + private readonly IAccountService _accountService; + private readonly CurrentAccountService _currentAccountService; + + public RegisterScenario(IAccountService accountService, CurrentAccountService currentAccountService) + { + _accountService = accountService; + _currentAccountService = currentAccountService; + } + + public string Name => "Register"; + + public void Run() + { + string pin1 = AnsiConsole.Ask("Enter your pin: "); + string pin2 = AnsiConsole.Ask("Re-enter your pin: "); + + if (pin1 != pin2) Run(); + + string hashedPin = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(pin1))); + + long id = _accountService.Create(hashedPin); + + AnsiConsole.WriteLine(id.ToString(CultureInfo.CurrentCulture)); + } + + public void SetNext(IScenario scenario) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Lab5/ATM.Presentation.Console/Scenarios/Register/RegisterScenarioProvider.cs b/src/Lab5/ATM.Presentation.Console/Scenarios/Register/RegisterScenarioProvider.cs new file mode 100644 index 0000000..80c8afc --- /dev/null +++ b/src/Lab5/ATM.Presentation.Console/Scenarios/Register/RegisterScenarioProvider.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using ATM.Presentation.Console.CurrentAccount; +using AutomatedTellerMachine.Contracts.Accounts; + +namespace ATM.Presentation.Console.Scenarios.Register; + +public class RegisterScenarioProvider : IScenarioProvider +{ + private readonly IAccountService _service; + private readonly CurrentAccountService _currentAccountService; + + public RegisterScenarioProvider( + IAccountService service, CurrentAccountService currentAccountService) + { + _service = service; + _currentAccountService = currentAccountService; + } + + public bool TryGetScenario( + [NotNullWhen(true)] out IScenario? scenario) + { + if (_currentAccountService.CurrentAccount == null) + { + scenario = new RegisterScenario(_service, _currentAccountService); + return true; + } + + scenario = null; + return false; + } +} \ No newline at end of file diff --git a/src/Lab5/AutomatedTellerMachine.Ports/AutomatedTellerMachine.Ports.csproj b/src/Lab5/AutomatedTellerMachine.Ports/AutomatedTellerMachine.Ports.csproj new file mode 100644 index 0000000..0542335 --- /dev/null +++ b/src/Lab5/AutomatedTellerMachine.Ports/AutomatedTellerMachine.Ports.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/src/Lab5/AutomatedTellerMachine/AutomatedTellerMachine.csproj b/src/Lab5/AutomatedTellerMachine/AutomatedTellerMachine.csproj new file mode 100644 index 0000000..ffcffe2 --- /dev/null +++ b/src/Lab5/AutomatedTellerMachine/AutomatedTellerMachine.csproj @@ -0,0 +1,22 @@ + + + + Exe + net7.0 + enable + enable + Lab5 + + + + + + + + + + + + + + diff --git a/src/Lab5/AutomatedTellerMachine/Program.cs b/src/Lab5/AutomatedTellerMachine/Program.cs new file mode 100644 index 0000000..e94a2c5 --- /dev/null +++ b/src/Lab5/AutomatedTellerMachine/Program.cs @@ -0,0 +1,37 @@ +// See https://aka.ms/new-console-template for more information + +using ATM.Infrastructure.DataAccess.Extensions; +using ATM.Presentation.Console; +using ATM.Presentation.Console.Extensions; +using AutomatedTellerMachine.Application.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console; + +var collection = new ServiceCollection(); + +collection + .AddApplication() + .AddInfrastructureDataAccess(configuration => + { + configuration.Host = "localhost"; + configuration.Port = 6432; + configuration.Username = "postgres"; + configuration.Password = "postgres"; + configuration.Database = "postgres"; + configuration.SslMode = "Prefer"; + }) + .AddPresentationConsole(); + +ServiceProvider provider = collection.BuildServiceProvider(); +using IServiceScope scope = provider.CreateScope(); + +scope.UseInfrastructureDataAccess(); + +ScenarioRunner scenarioRunner = scope.ServiceProvider + .GetRequiredService(); + +while (true) +{ + scenarioRunner.Run(); + AnsiConsole.Clear(); +} \ No newline at end of file diff --git a/src/Lab5/CommandLine/Class1.cs b/src/Lab5/CommandLine/Class1.cs new file mode 100644 index 0000000..89ccea6 --- /dev/null +++ b/src/Lab5/CommandLine/Class1.cs @@ -0,0 +1,6 @@ +namespace CommandLine +{ + public class Class1 + { + } +} \ No newline at end of file diff --git a/src/Lab5/CommandLine/CommandLine.csproj b/src/Lab5/CommandLine/CommandLine.csproj new file mode 100644 index 0000000..2ad9d3a --- /dev/null +++ b/src/Lab5/CommandLine/CommandLine.csproj @@ -0,0 +1,54 @@ + + + + + Debug + AnyCPU + {61B79AE5-7777-4388-AC9B-A0AEB56D105F} + Library + Properties + CommandLine + CommandLine + v4.8 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + diff --git a/src/Lab5/CommandLine/Properties/AssemblyInfo.cs b/src/Lab5/CommandLine/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dd9be74 --- /dev/null +++ b/src/Lab5/CommandLine/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CommandLine")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CommandLine")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("61B79AE5-7777-4388-AC9B-A0AEB56D105F")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Lab5/PostgreSql/IDatabase.cs b/src/Lab5/PostgreSql/IDatabase.cs new file mode 100644 index 0000000..f353d48 --- /dev/null +++ b/src/Lab5/PostgreSql/IDatabase.cs @@ -0,0 +1,7 @@ +namespace PostgreSql +{ + public interface IDatabase + { + + } +} \ No newline at end of file diff --git a/src/Lab5/PostgreSql/PostgreSql.csproj b/src/Lab5/PostgreSql/PostgreSql.csproj new file mode 100644 index 0000000..9d10410 --- /dev/null +++ b/src/Lab5/PostgreSql/PostgreSql.csproj @@ -0,0 +1,54 @@ + + + + + Debug + AnyCPU + {896B50C3-CEF3-461E-89C4-2499DBD8F6A4} + Library + Properties + PostgreSql + PostgreSql + v4.8 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + diff --git a/src/Lab5/PostgreSql/Properties/AssemblyInfo.cs b/src/Lab5/PostgreSql/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..740bb2e --- /dev/null +++ b/src/Lab5/PostgreSql/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PostgreSql")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PostgreSql")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("896B50C3-CEF3-461E-89C4-2499DBD8F6A4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Lab5/docker-compose.yaml b/src/Lab5/docker-compose.yaml new file mode 100644 index 0000000..d2e1e9b --- /dev/null +++ b/src/Lab5/docker-compose.yaml @@ -0,0 +1,12 @@ +version: '3.9' + +services: + postgres: + image: postgres:latest + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + ports: + - "6432:5432" + restart: unless-stopped \ No newline at end of file