From 3b2b4c9b7ed3de6a10c3d45484f5324b07135b36 Mon Sep 17 00:00:00 2001 From: Mateusz Skoczek Date: Tue, 23 Jan 2024 15:41:59 +0100 Subject: [PATCH] final1 --- .gitignore | 2 +- Dockerfile => Main.Dockerfile | 9 +- Nginx.Dockerfile | 6 + Nginx/localhost.conf | 31 ++ Nginx/localhost.crt | 22 ++ Nginx/localhost.key | 28 ++ Nginx/nginx.conf | 43 ++ .../AuthenticationHelper.cs | 12 +- .../AccountsController.cs | 124 ++++-- .../BalanceController.cs | 71 ++++ .../SecureBank.API.Controllers.csproj | 2 + .../TransfersController.cs | 103 +++++ .../EncryptionConfiguration.cs | 32 ++ .../EncryptionHelper.cs | 75 ++++ .../SecureBank.API.Encryption.csproj | 13 + .../Attributes/RequiresClaimAttribute.cs | 47 +++ .../SecureBank.API.Helpers.csproj | 9 + .../AccountsService.cs | 369 +++++++++++++++--- .../SecureBank.API.Services/BalanceService.cs | 81 ++++ .../SecureBank.API.Services.csproj | 6 + .../TransfersService.cs | 250 ++++++++++++ SecureBank.Authentication/Claims.cs | 41 ++ .../SecureBank.Authentication.csproj | 0 SecureBank.Common/APIResponse.cs | 12 +- SecureBank.Common/Accounts/AccountResponse.cs | 78 ++++ .../Accounts/ChangePasswordRequest.cs | 17 + .../Accounts/CreateAccountRequest.cs | 12 + SecureBank.Common/ResponseStatus.cs | 15 + .../Transfers/CreateAdminTransferRequest.cs | 45 +++ .../Transfers/CreateUserTransferRequest.cs | 34 ++ .../Transfers/TransferResponse.cs | 54 +++ SecureBank.Database/Account.cs | 31 ++ SecureBank.Database/DatabaseContext.cs | 1 + .../20240114165546_Migration1.Designer.cs | 132 ------- .../20240114170227_Migration2.Designer.cs | 138 ------- .../Migrations/20240114170227_Migration2.cs | 39 -- .../20240115084527_Migration3.Designer.cs | 169 -------- .../Migrations/20240115084527_Migration3.cs | 46 --- .../Migrations/20240115132220_Migration4.cs | 29 -- ... => 20240122225115_Migration1.Designer.cs} | 91 ++++- ...ation1.cs => 20240122225115_Migration1.cs} | 67 +++- .../DatabaseContextModelSnapshot.cs | 87 +++++ SecureBank.Database/Transfer.cs | 52 +++ SecureBank.Helpers/AuthenticationHelper.cs | 39 -- .../SecureBank.Website.API/APIClient.cs | 136 +++++-- .../APIEndpointsConfiguration.cs | 30 ++ .../SecureBank.Website.API/APIMethodType.cs | 1 + .../SecureBank.Website.Authentication.csproj | 2 +- .../TokenAuthenticationStateProvider.cs | 15 +- .../AccountsService.cs | 38 +- .../BalanceService.cs | 58 +++ .../TransfersService.cs | 72 ++++ SecureBank.sln | 30 +- SecureBank/Components/Layout/MainLayout.razor | 67 +++- SecureBank/Components/Layout/NavMenu.razor | 27 ++ .../Components/Layout/NavMenu.razor.css | 16 + .../Components/Pages/Admin/Accounts.razor | 185 +++++++++ .../Pages/Admin/CreateTransfer.razor | 337 ++++++++++++++++ SecureBank/Components/Pages/Admin/Panel.razor | 70 ++++ .../Components/Pages/Admin/Register.razor | 199 +++++++--- SecureBank/Components/Pages/Auth.razor | 251 ++++++++---- SecureBank/Components/Pages/Home.razor | 4 +- .../Pages/User/AccountDetails.razor | 196 ++++++++++ .../Pages/User/ChangePassword.razor | 140 +++++++ .../Pages/User/CreateTransfer.razor | 255 ++++++++++++ .../Components/Pages/User/Dashboard.razor | 152 ++++++++ SecureBank/Components/_Imports.razor | 11 +- SecureBank/Program.cs | 30 +- SecureBank/Properties/launchSettings.json | 3 +- SecureBank/SecureBank.csproj | 12 +- SecureBank/appsettings.Development.json | 10 +- SecureBank/appsettings.Production.json | 8 + SecureBank/appsettings.json | 26 +- SecureBank/wwwroot/app.css | 22 ++ database-default.db | Bin 0 -> 69632 bytes docker-compose.yml | 21 + 76 files changed, 4100 insertions(+), 888 deletions(-) rename Dockerfile => Main.Dockerfile (86%) create mode 100644 Nginx.Dockerfile create mode 100644 Nginx/localhost.conf create mode 100644 Nginx/localhost.crt create mode 100644 Nginx/localhost.key create mode 100644 Nginx/nginx.conf create mode 100644 SecureBank.API/SecureBank.API.Controllers/BalanceController.cs create mode 100644 SecureBank.API/SecureBank.API.Controllers/TransfersController.cs create mode 100644 SecureBank.API/SecureBank.API.Encryption/EncryptionConfiguration.cs create mode 100644 SecureBank.API/SecureBank.API.Encryption/EncryptionHelper.cs create mode 100644 SecureBank.API/SecureBank.API.Encryption/SecureBank.API.Encryption.csproj create mode 100644 SecureBank.API/SecureBank.API.Helpers/Attributes/RequiresClaimAttribute.cs create mode 100644 SecureBank.API/SecureBank.API.Services/BalanceService.cs create mode 100644 SecureBank.API/SecureBank.API.Services/TransfersService.cs create mode 100644 SecureBank.Authentication/Claims.cs rename SecureBank.Helpers/SecureBank.Helpers.csproj => SecureBank.Authentication/SecureBank.Authentication.csproj (100%) create mode 100644 SecureBank.Common/Accounts/AccountResponse.cs create mode 100644 SecureBank.Common/Accounts/ChangePasswordRequest.cs create mode 100644 SecureBank.Common/ResponseStatus.cs create mode 100644 SecureBank.Common/Transfers/CreateAdminTransferRequest.cs create mode 100644 SecureBank.Common/Transfers/CreateUserTransferRequest.cs create mode 100644 SecureBank.Common/Transfers/TransferResponse.cs delete mode 100644 SecureBank.Database/Migrations/20240114165546_Migration1.Designer.cs delete mode 100644 SecureBank.Database/Migrations/20240114170227_Migration2.Designer.cs delete mode 100644 SecureBank.Database/Migrations/20240114170227_Migration2.cs delete mode 100644 SecureBank.Database/Migrations/20240115084527_Migration3.Designer.cs delete mode 100644 SecureBank.Database/Migrations/20240115084527_Migration3.cs delete mode 100644 SecureBank.Database/Migrations/20240115132220_Migration4.cs rename SecureBank.Database/Migrations/{20240115132220_Migration4.Designer.cs => 20240122225115_Migration1.Designer.cs} (64%) rename SecureBank.Database/Migrations/{20240114165546_Migration1.cs => 20240122225115_Migration1.cs} (51%) create mode 100644 SecureBank.Database/Transfer.cs delete mode 100644 SecureBank.Helpers/AuthenticationHelper.cs create mode 100644 SecureBank.Website/SecureBank.Website.Services/BalanceService.cs create mode 100644 SecureBank.Website/SecureBank.Website.Services/TransfersService.cs create mode 100644 SecureBank/Components/Pages/Admin/Accounts.razor create mode 100644 SecureBank/Components/Pages/Admin/CreateTransfer.razor create mode 100644 SecureBank/Components/Pages/Admin/Panel.razor create mode 100644 SecureBank/Components/Pages/User/AccountDetails.razor create mode 100644 SecureBank/Components/Pages/User/ChangePassword.razor create mode 100644 SecureBank/Components/Pages/User/CreateTransfer.razor create mode 100644 SecureBank/Components/Pages/User/Dashboard.razor create mode 100644 SecureBank/appsettings.Production.json create mode 100644 database-default.db create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index d7385e6..fdc82ba 100644 --- a/.gitignore +++ b/.gitignore @@ -398,4 +398,4 @@ FodyWeavers.xsd *.sln.iml # SQLite database -*.db \ No newline at end of file +database.db \ No newline at end of file diff --git a/Dockerfile b/Main.Dockerfile similarity index 86% rename from Dockerfile rename to Main.Dockerfile index 1e2d1e4..8f59dcd 100644 --- a/Dockerfile +++ b/Main.Dockerfile @@ -1,9 +1,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base USER app WORKDIR /app -EXPOSE 443 -EXPOSE 80 -ENV ASPNETCORE_URLS=https://+:443;http://+:80 +EXPOSE 8080 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release @@ -15,9 +13,10 @@ COPY ["SecureBank.API/SecureBank.API.Controllers/SecureBank.API.Controllers.cspr COPY ["SecureBank.Common/SecureBank.Common.csproj", "SecureBank.Common/"] COPY ["SecureBank.API/SecureBank.API.Services/SecureBank.API.Services.csproj", "SecureBank.API/SecureBank.API.Services/"] COPY ["SecureBank.Extensions/SecureBank.Extensions.csproj", "SecureBank.Extensions/"] +COPY ["SecureBank.Authentication/SecureBank.Authentication.csproj", "SecureBank.Authentication/"] COPY ["SecureBank.API/SecureBank.API.Helpers/SecureBank.API.Helpers.csproj", "SecureBank.API/SecureBank.API.Helpers/"] +COPY ["SecureBank.API/SecureBank.API.Encryption/SecureBank.API.Encryption.csproj", "SecureBank.API/SecureBank.API.Encryption/"] COPY ["SecureBank.Website/SecureBank.Website.Authentication/SecureBank.Website.Authentication.csproj", "SecureBank.Website/SecureBank.Website.Authentication/"] -COPY ["SecureBank.Helpers/SecureBank.Helpers.csproj", "SecureBank.Helpers/"] COPY ["SecureBank.Website/SecureBank.Website.Services/SecureBank.Website.Services.csproj", "SecureBank.Website/SecureBank.Website.Services/"] COPY ["SecureBank.Website/SecureBank.Website.API/SecureBank.Website.API.csproj", "SecureBank.Website/SecureBank.Website.API/"] RUN dotnet restore "./SecureBank/./SecureBank.csproj" @@ -32,4 +31,6 @@ RUN dotnet publish "./SecureBank.csproj" -c $BUILD_CONFIGURATION -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . +COPY database-default.db database.db +USER root ENTRYPOINT ["dotnet", "SecureBank.dll"] \ No newline at end of file diff --git a/Nginx.Dockerfile b/Nginx.Dockerfile new file mode 100644 index 0000000..8ca8f3f --- /dev/null +++ b/Nginx.Dockerfile @@ -0,0 +1,6 @@ +FROM nginx:alpine +ARG KEY_PASSWORD=default + +COPY ./Nginx/nginx.conf /etc/nginx/nginx.conf +COPY ./Nginx/localhost.crt /etc/ssl/certs/localhost.crt +COPY ./Nginx/localhost.key /etc/ssl/private/localhost.key \ No newline at end of file diff --git a/Nginx/localhost.conf b/Nginx/localhost.conf new file mode 100644 index 0000000..b99eb26 --- /dev/null +++ b/Nginx/localhost.conf @@ -0,0 +1,31 @@ +[req] +default_bits = 2048 +default_keyfile = localhost.key +distinguished_name = req_distinguished_name +req_extensions = req_ext +x509_extensions = v3_ca + +[req_distinguished_name] +countryName = Country Name (2 letter code) +countryName_default = PL +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Mazowieckie +localityName = Locality Name (eg, city) +localityName_default = Warszawa +organizationName = Organization Name (eg, company) +organizationName_default = localhost +organizationalUnitName = organizationalunit +organizationalUnitName_default = Production +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_default = localhost +commonName_max = 64 + +[req_ext] +subjectAltName = @alt_names + +[v3_ca] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = 127.0.0.1 \ No newline at end of file diff --git a/Nginx/localhost.crt b/Nginx/localhost.crt new file mode 100644 index 0000000..b37f3eb --- /dev/null +++ b/Nginx/localhost.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtjCCAp6gAwIBAgIUcyGfC/bnYYhO8GSeVSiRc7Kkr2IwDQYJKoZIhvcNAQEL +BQAwczELMAkGA1UEBhMCUEwxFDASBgNVBAgMC01hem93aWVja2llMREwDwYDVQQH +DAhXYXJzemF3YTESMBAGA1UECgwJbG9jYWxob3N0MRMwEQYDVQQLDApQcm9kdWN0 +aW9uMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjQwMTE5MTgyNzIwWhcNMjUwMTE4 +MTgyNzIwWjBzMQswCQYDVQQGEwJQTDEUMBIGA1UECAwLTWF6b3dpZWNraWUxETAP +BgNVBAcMCFdhcnN6YXdhMRIwEAYDVQQKDAlsb2NhbGhvc3QxEzARBgNVBAsMClBy +b2R1Y3Rpb24xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAO7Pf3zgx31E2BDEAvCAzxBHnHaKSQqXaoOGFlltJ0jkDxFb +Vyowjmt3VEX2k91QX+W1hguzaRJlxmNpL6IAhvidWywu9NG8TjwkW8CifvT71rL5 +c0jioSnbbxPUgQAeS6bcNW3RlL9umBQOVOet6ELnecq2iAkriPjT6Hpf6HXKXxUM +kDnk4dYNKhp78l8tqeZp4kW90PWg2zOaw129ZwSZ+EIUgVlqBKzq0/f9JDxQoTNE +ynZ4HRYveIfJtWLLG2s2i7pUBUrPCk1YzquN+mXfhCkjcBT7awwu+R4/1umVCrIy +DaVEq0aJzfXJ3D1wxa5zdp3z5/V0Pa2JicTXxB0CAwEAAaNCMEAwHwYDVR0RBBgw +FoIJbG9jYWxob3N0ggkxMjcuMC4wLjEwHQYDVR0OBBYEFEbrV6mArhT6VoJjvyvZ +sDhNY+OjMA0GCSqGSIb3DQEBCwUAA4IBAQAw0pYip/FamfiioGpg+QQ96Ef4zSOB +/tRC6nVob4YAO+mpim3LsT0JKS82Xe8ylZ2OgQ28/KQDdNffpnojgVR5Q15e/HqB +TK3aWUJ4vgFzvZ6mDhXTMOLXG9gLncxQM9YuyfOMm6ru3BNW7l2vPPPP+GQYHhkg +lIVv64Z0kHR5iNJRg2Ji6kR8tELd3EckxvVAMqwvsreMVTzej9bvR6OW65mGNmFE +qaG8CqZqysk7uzfE3kDfgN4Z6p7eAnILkPxFBGt4iCQ/N5vgF9TDe0smcoRvm/ie +Ekg0Y45ozFz4Tu/yHv4sKs5U69knz6fUu15GOCWk+EMUkFu+h0wPJtEA +-----END CERTIFICATE----- diff --git a/Nginx/localhost.key b/Nginx/localhost.key new file mode 100644 index 0000000..5833429 --- /dev/null +++ b/Nginx/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDuz3984Md9RNgQ +xALwgM8QR5x2ikkKl2qDhhZZbSdI5A8RW1cqMI5rd1RF9pPdUF/ltYYLs2kSZcZj +aS+iAIb4nVssLvTRvE48JFvAon70+9ay+XNI4qEp228T1IEAHkum3DVt0ZS/bpgU +DlTnrehC53nKtogJK4j40+h6X+h1yl8VDJA55OHWDSoae/JfLanmaeJFvdD1oNsz +msNdvWcEmfhCFIFZagSs6tP3/SQ8UKEzRMp2eB0WL3iHybViyxtrNou6VAVKzwpN +WM6rjfpl34QpI3AU+2sMLvkeP9bplQqyMg2lRKtGic31ydw9cMWuc3ad8+f1dD2t +iYnE18QdAgMBAAECggEABQkvIQ0EZ+nZvJDFlxjARRGdhQppe+Wxg1CCjCQ3Hdxh +TPZmIief+Tgs+MS4XYRiYU+wofKIlrC6vEHtlTb5oah/0bCzzPBxnecOYEOM9dGR +t3h3K+RLjbxo3Twv9DavVbdzEPvIbTkBz/p1Y+VQ7og/Ez1CJR9b5hNh5aK8Hi+5 +37v1m2jtCWu9aMcz6SFQbi0o4qN3ZCKK9qlTbr6AIoFJosIOsMS6szoNcYNIYyxl +V4MogBZ0IGU+nC9GTb8XOsfUd2c8lStOMEhrc2F1RzwgnQiUF4W2n04L7yZ32X92 +v8cJQq44wLdU64KFqzKQGfQvISqFLpb08HjNv+WWqQKBgQD6np8dzRvC4E+8Xapz +ablPGuvOdsFQNIUF99xXVVpRhZg6XSsZvPOClgolNUmkG0B2kbsHWmsczc8MCBjD +/0PC2vMeD/AImiaoayFu4dDysgWId9iVT7OUM72+spKH3zox8TTvSBx+qd9xUHec +PIv5KzIY58m9ZwPFHjUR4m7xRwKBgQDz7/mcCQdHRxnk73XcQeqeUPC6j5HMxq2Y +/jDh1OcugJiABBa0hH1Gc1uOnAl4Q74yDryD5cjLJasS8u5aC3ea5yEdCyPlpTGT ++XGWp9tJhOkza2v9NNSYC9CU90XCor+SIhf4oK4FicFmH0/PD0ypl5QnAFM5ilaf +77Oj12rxewKBgAk2VXD5/iA+sI+i/cX6R+aBfdN8CAUTuSQMBVxsdcJzX7IdhJ0Q +lf7h1wOhHtDac5coKjDOAQvxAMBXd9mUg4AhHjinq1IVoIAmV/dEc7LIGm32wc5T +PK2g7UOaOsqlyPTXAfQduXZqdh0rMQpcK2UAUnoZ4w+EPt47CwZaRWu3AoGBAJNv +BRQzuLxx0sq1mCyJgn4xOW3ofByiMCX57B1yCk/m1UT6M6bjNLwx2LJ2yJpxhzDG +C1ZEuXleyOjo/vpZV+69pxvgsc+IalCfQPHnffgPZsr3MAuXtK445dZDJVyf9N0j +0g0pQESEn+lTH29sNc+Cl76CycpLPFPqEk8CUdalAoGAFGkmys1mo83b3Rcbqkmh +Gl0LSKYwbbO+rJWS39NyXP/iI5Aykrx0v8N7/wTa/XWPtAD3MzQ04GAcG/VcqhDp +fMQURYBnRJlvctWxrTTBnBqlbDTM39i/wGqDt5CPLICT17rPFjWfUGrRX0sdsND1 +ZMXdlsU48M++9GCs6VFs4ks= +-----END PRIVATE KEY----- diff --git a/Nginx/nginx.conf b/Nginx/nginx.conf new file mode 100644 index 0000000..9b6336a --- /dev/null +++ b/Nginx/nginx.conf @@ -0,0 +1,43 @@ +worker_processes 1; + +events { worker_connections 1024; } + +http { + + sendfile on; + + upstream securebank { + server securebank-website:8080; + } + + server { + listen 80; + server_name localhost; + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/ssl/certs/localhost.crt; + ssl_certificate_key /etc/ssl/private/localhost.key; + + location / { + proxy_pass http://securebank; + proxy_redirect off; + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $server_name; + } + } +} \ No newline at end of file diff --git a/SecureBank.API/SecureBank.API.Authentication/AuthenticationHelper.cs b/SecureBank.API/SecureBank.API.Authentication/AuthenticationHelper.cs index 8abf828..41a7e1d 100644 --- a/SecureBank.API/SecureBank.API.Authentication/AuthenticationHelper.cs +++ b/SecureBank.API/SecureBank.API.Authentication/AuthenticationHelper.cs @@ -36,7 +36,7 @@ namespace SecureBank.API.Authentication #region METHODS - public string GenerateToken(Guid tokenId, int accountId, bool oneTimeToken = false) + public string GenerateToken(Guid tokenId, Account account, bool oneTimeToken = false) { DateTime expirationTime = DateTime.UtcNow.AddMinutes(_configuration.TokenLifetime); @@ -44,11 +44,13 @@ namespace SecureBank.API.Authentication { Subject = new ClaimsIdentity(new List { - new Claim(JwtRegisteredClaimNames.Jti, tokenId.ToString()), - new Claim(JwtRegisteredClaimNames.Sub, accountId.ToString()), - new Claim(JwtRegisteredClaimNames.Exp, expirationTime.ToString()), + new Claim("jti", tokenId.ToString()), + new Claim("uid", account.Id.ToString()), + new Claim("first_name", account.FirstName), + new Claim("last_name", account.LastName), + new Claim("exp", expirationTime.ToString()), new Claim("one_time_token", oneTimeToken.ToString()), - new Claim("admin", "false"), //TODO: w zależności od użytkownika + new Claim("admin", account.IsAdmin.ToString()), }), Expires = expirationTime, Issuer = _configuration.TokenIssuer, diff --git a/SecureBank.API/SecureBank.API.Controllers/AccountsController.cs b/SecureBank.API/SecureBank.API.Controllers/AccountsController.cs index 5bf74d6..441be19 100644 --- a/SecureBank.API/SecureBank.API.Controllers/AccountsController.cs +++ b/SecureBank.API/SecureBank.API.Controllers/AccountsController.cs @@ -1,12 +1,18 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Identity.Client; +using SecureBank.API.Authentication; using SecureBank.API.Services; +using SecureBank.Authentication; using SecureBank.Common; using SecureBank.Common.Accounts; +using SecureBank.Helpers.Attributes; using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Text; using System.Threading.Tasks; using static System.Runtime.InteropServices.JavaScript.JSType; @@ -40,18 +46,17 @@ namespace SecureBank.API.Controllers [HttpPost] [Route("create-account")] - [AllowAnonymous] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [RequiresClaim("admin", "True")] public async Task>> CreateAccount([FromBody] CreateAccountRequest data) { APIResponse response = await _accountsService.CreateAccount(data); - if (response.Success) + return response.Status switch { - return Ok(response); - } - else - { - return BadRequest(response); - } + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; } [HttpGet] @@ -60,35 +65,102 @@ namespace SecureBank.API.Controllers public async Task>> GetPasswordVariant([FromRoute(Name = "account_id")] int accountId) { APIResponse response = await _accountsService.GetPasswordVariant(accountId); - if (response.Success) + return response.Status switch { - return Ok(response); - } - else - { - return BadRequest(response); - } + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; } [HttpPost] - [Route("{account_id}/authentication")] + [Route("authentication")] [AllowAnonymous] /* * Action codes: * 1 - Go back to client code input - * 2 - Failed login count increment + * 2 - Change password required */ - public async Task>> Authentication([FromRoute(Name = "account_id")] int accountId, [FromBody] AuthenticationRequest data) + public async Task>> Authentication([FromBody] AuthenticationRequest data) { - APIResponse response = await _accountsService.Authentication(accountId, data); - if (response.Success) + APIResponse response = await _accountsService.Authentication(data); + return response.Status switch { - return Ok(response); - } - else + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpPost] + [Route("authentication-refresh")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public async Task>> AuthenticationRefresh() + { + APIResponse response = await _accountsService.AuthenticationRefresh(new Claims(User.Claims)); + return response.Status switch { - return BadRequest(response); - } + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpPatch] + [Route("change-password")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public async Task> ChangePassword([FromBody] ChangePasswordRequest data) + { + APIResponse response = await _accountsService.ChangePassword(new Claims(User.Claims), data); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpGet] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public async Task>>> GetAccounts([FromQuery]int? id, [FromQuery] string? iban) + { + APIResponse> response = await _accountsService.GetAccounts(iban, id, new Claims(User.Claims)); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpPatch] + [Route("{account_id}/reset-password")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [RequiresClaim("admin", "True")] + public async Task> ResetPassword([FromRoute(Name = "account_id")] int accountId) + { + APIResponse response = await _accountsService.ResetPassword(accountId); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpPatch] + [Route("{account_id}/unlock")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [RequiresClaim("admin", "True")] + public async Task> UnlockAccount([FromRoute(Name = "account_id")] int accountId) + { + APIResponse response = await _accountsService.UnlockAccount(accountId); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; } #endregion diff --git a/SecureBank.API/SecureBank.API.Controllers/BalanceController.cs b/SecureBank.API/SecureBank.API.Controllers/BalanceController.cs new file mode 100644 index 0000000..b92ee4f --- /dev/null +++ b/SecureBank.API/SecureBank.API.Controllers/BalanceController.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SecureBank.API.Services; +using SecureBank.Authentication; +using SecureBank.Common; +using SecureBank.Helpers.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.API.Controllers +{ + [ApiController] + [Route("api/balance")] + public class BalanceController : ControllerBase + { + #region SERVICES + + private IBalanceService _balanceService; + + #endregion + + + + #region CONSTRUCTORS + + public BalanceController(IBalanceService balanceService) + { + _balanceService = balanceService; + } + + #endregion + + + + #region METHODS + + [HttpGet] + [Route("{account_id}")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [RequiresClaim("admin", "True")] + public async Task>> GetAccountBalance([FromRoute(Name = "account_id")]int accountId) + { + APIResponse response = await _balanceService.GetAccountBalance(accountId); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpGet] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public async Task>> GetBalance() + { + APIResponse response = await _balanceService.GetBalance(new Claims(User.Claims)); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + #endregion + } +} diff --git a/SecureBank.API/SecureBank.API.Controllers/SecureBank.API.Controllers.csproj b/SecureBank.API/SecureBank.API.Controllers/SecureBank.API.Controllers.csproj index 8c82823..d32ab4c 100644 --- a/SecureBank.API/SecureBank.API.Controllers/SecureBank.API.Controllers.csproj +++ b/SecureBank.API/SecureBank.API.Controllers/SecureBank.API.Controllers.csproj @@ -7,12 +7,14 @@ + + diff --git a/SecureBank.API/SecureBank.API.Controllers/TransfersController.cs b/SecureBank.API/SecureBank.API.Controllers/TransfersController.cs new file mode 100644 index 0000000..35e0edb --- /dev/null +++ b/SecureBank.API/SecureBank.API.Controllers/TransfersController.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SecureBank.API.Services; +using SecureBank.Authentication; +using SecureBank.Common; +using SecureBank.Common.Transfers; +using SecureBank.Helpers.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace SecureBank.API.Controllers +{ + [ApiController] + [Route("api/transfers")] + public class TransfersController : ControllerBase + { + #region SERVICES + + private ITransfersService _transfersService; + + #endregion + + + + #region CONSTRUCTORS + + public TransfersController(ITransfersService transfersService) + { + _transfersService = transfersService; + } + + #endregion + + + + #region METHODS + + [HttpGet] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public async Task>>> GetTransfers() + { + APIResponse> response = await _transfersService.GetTransfers(new Claims(User.Claims)); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + + [HttpGet] + [Route("{account_id}")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [RequiresClaim("admin", "True")] + public async Task>>> GetUserTransfers([FromRoute(Name = "account_id")]int accountId) + { + APIResponse> response = await _transfersService.GetUserTransfers(accountId); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpPost] + [Route("admin-transfer")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [RequiresClaim("admin", "True")] + public async Task> CreateAdminTransfer([FromBody]CreateAdminTransferRequest data) + { + APIResponse response = await _transfersService.CreateAdminTransfer(data); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + [HttpPost] + [Route("user-transfer")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public async Task> CreateUserTransfer([FromBody] CreateUserTransferRequest data) + { + APIResponse response = await _transfersService.CreateUserTransfer(data, new Claims(User.Claims)); + return response.Status switch + { + ResponseStatus.Ok => Ok(response), + ResponseStatus.BadRequest => BadRequest(response), + ResponseStatus.Unauthorized => Unauthorized(response), + }; + } + + #endregion + } +} diff --git a/SecureBank.API/SecureBank.API.Encryption/EncryptionConfiguration.cs b/SecureBank.API/SecureBank.API.Encryption/EncryptionConfiguration.cs new file mode 100644 index 0000000..07a88a7 --- /dev/null +++ b/SecureBank.API/SecureBank.API.Encryption/EncryptionConfiguration.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.API.Encryption +{ + public class EncryptionConfiguration + { + #region PROPERTIES + + public byte[] Key { get; private set; } + public byte[] IV { get; private set; } + + #endregion + + + + #region CONSTRUCTORS + + public EncryptionConfiguration(IConfiguration configuration) + { + Key = Encoding.UTF8.GetBytes(configuration.GetSection("Encryption")["Key"]); + IV = Encoding.UTF8.GetBytes(configuration.GetSection("Encryption")["IV"]); + } + + #endregion + } +} diff --git a/SecureBank.API/SecureBank.API.Encryption/EncryptionHelper.cs b/SecureBank.API/SecureBank.API.Encryption/EncryptionHelper.cs new file mode 100644 index 0000000..4a59639 --- /dev/null +++ b/SecureBank.API/SecureBank.API.Encryption/EncryptionHelper.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace SecureBank.API.Encryption +{ + public class EncryptionHelper + { + #region SERVICES + + private EncryptionConfiguration _configuration; + + #endregion + + + + #region FIELDS + + private Aes _aes; + + #endregion + + + + #region CONSTRUCTORS + + public EncryptionHelper(EncryptionConfiguration configuration) + { + _configuration = configuration; + + _aes = Aes.Create(); + _aes.Key = _configuration.Key; + _aes.IV = _configuration.IV; + } + + #endregion + + + + #region PUBLIC METHODS + + public byte[] Encrypt(string data) + { + ICryptoTransform encryptor = _aes.CreateEncryptor(_aes.Key, _aes.IV); + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) + using (StreamWriter streamWriter = new StreamWriter(cryptoStream)) + { + streamWriter.Write(data); + } + return memoryStream.ToArray(); + } + } + + public string Decrypt(byte[] data) + { + ICryptoTransform decryptor = _aes.CreateDecryptor(_configuration.Key, _configuration.IV); + using (MemoryStream memoryStream = new MemoryStream(data)) + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) + { + byte[] outputBytes = new byte[data.Length]; + int decryptedByteCount = cryptoStream.Read(outputBytes, 0, outputBytes.Length); + return Encoding.UTF8.GetString(outputBytes.Take(decryptedByteCount).ToArray()); + } + } + + #endregion + } +} diff --git a/SecureBank.API/SecureBank.API.Encryption/SecureBank.API.Encryption.csproj b/SecureBank.API/SecureBank.API.Encryption/SecureBank.API.Encryption.csproj new file mode 100644 index 0000000..a4bec6e --- /dev/null +++ b/SecureBank.API/SecureBank.API.Encryption/SecureBank.API.Encryption.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/SecureBank.API/SecureBank.API.Helpers/Attributes/RequiresClaimAttribute.cs b/SecureBank.API/SecureBank.API.Helpers/Attributes/RequiresClaimAttribute.cs new file mode 100644 index 0000000..716a02a --- /dev/null +++ b/SecureBank.API/SecureBank.API.Helpers/Attributes/RequiresClaimAttribute.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.Helpers.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class RequiresClaimAttribute : Attribute, IAuthorizationFilter + { + #region FIELDS + + private readonly string _claimName; + private readonly string _claimValue; + + #endregion + + + + #region CONSTRUCTORS + + public RequiresClaimAttribute(string claimName, string claimValue) + { + _claimName = claimName; + _claimValue = claimValue; + } + + #endregion + + + + #region PUBLIC METHODS + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext.User.HasClaim(_claimName, _claimValue)) + { + context.Result = new ForbidResult(); + } + } + + #endregion + } +} diff --git a/SecureBank.API/SecureBank.API.Helpers/SecureBank.API.Helpers.csproj b/SecureBank.API/SecureBank.API.Helpers/SecureBank.API.Helpers.csproj index 57961af..b585601 100644 --- a/SecureBank.API/SecureBank.API.Helpers/SecureBank.API.Helpers.csproj +++ b/SecureBank.API/SecureBank.API.Helpers/SecureBank.API.Helpers.csproj @@ -6,8 +6,17 @@ enable + + + + + + + + + diff --git a/SecureBank.API/SecureBank.API.Services/AccountsService.cs b/SecureBank.API/SecureBank.API.Services/AccountsService.cs index 95fbbaa..970122c 100644 --- a/SecureBank.API/SecureBank.API.Services/AccountsService.cs +++ b/SecureBank.API/SecureBank.API.Services/AccountsService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using SecureBank.API.Helpers; using SecureBank.API.Authentication; +using SecureBank.Authentication; using SecureBank.Common; using SecureBank.Common.Accounts; using SecureBank.Database; @@ -15,6 +16,10 @@ using System.Runtime.Intrinsics.Arm; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using SecureBank.API.Encryption; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using Microsoft.Identity.Client; namespace SecureBank.API.Services { @@ -22,7 +27,12 @@ namespace SecureBank.API.Services { Task> CreateAccount(CreateAccountRequest data); Task> GetPasswordVariant(int accountId); - Task> Authentication(int accountId, AuthenticationRequest data); + Task> Authentication(AuthenticationRequest data); + Task> AuthenticationRefresh(Claims claims); + Task ChangePassword(Claims claims, ChangePasswordRequest data); + Task>> GetAccounts(string? iban, int? id, Claims claims); + Task ResetPassword(int accountId); + Task UnlockAccount(int accountId); } @@ -33,6 +43,8 @@ namespace SecureBank.API.Services private AuthenticationHelper _authenticationHelper; + private EncryptionHelper _encryptionHelper; + private DatabaseContext _database; private ILogger _logger; @@ -43,12 +55,11 @@ namespace SecureBank.API.Services #region CONSTRUCTORS - public AccountsService(AuthenticationHelper authenticationHelper, DatabaseContext database, ILogger logger) + public AccountsService(AuthenticationHelper authenticationHelper, EncryptionHelper encryptionHelper, DatabaseContext database, ILogger logger) { _authenticationHelper = authenticationHelper; - + _encryptionHelper = encryptionHelper; _database = database; - _logger = logger; } @@ -103,6 +114,31 @@ namespace SecureBank.API.Services CheckAction = new Predicate((x) => string.IsNullOrWhiteSpace(x.PhoneNumber)), Message = "Phone number cannot be empty" }, + new Check + { + CheckAction = new Predicate((x) => string.IsNullOrWhiteSpace(x.Address)), + Message = "Address cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => string.IsNullOrWhiteSpace(x.PESEL)), + Message = "PESEL cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => x.PESEL.Length != 11), + Message = "PESEL must be 11 charaters long" + }, + new Check + { + CheckAction = new Predicate((x) => string.IsNullOrWhiteSpace(x.IdCardNumber)), + Message = "Id card number cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => x.IdCardNumber.Length != 9), + Message = "Id card number must be 9 characters long" + }, }; foreach (Check check in checks) @@ -112,21 +148,44 @@ namespace SecureBank.API.Services return new APIResponse { Message = check.Message, - Success = false + Status = ResponseStatus.BadRequest, }; } } + byte[] pesel = _encryptionHelper.Encrypt(data.PESEL); + byte[] idCardNumber = _encryptionHelper.Encrypt(data.IdCardNumber); + byte[] cardCVV = _encryptionHelper.Encrypt(StringExtensions.CreateRandom(3, "1234567890")); + byte[] cardExpirationDate = _encryptionHelper.Encrypt(DateTime.Now.AddYears(5).ToString("MM/yy")); + Account account = new Account { FirstName = data.FirstName, LastName = data.LastName, Email = data.Email, PhoneNumber = data.PhoneNumber.Replace(" ", string.Empty), + Address = data.Address, + PESEL = pesel, + IdCardNumber = idCardNumber, + IBAN = string.Empty, + CardNumber = new byte[0], + CardCVV = cardCVV, + CardExpirationDate = cardExpirationDate }; await _database.Accounts.AddAsync(account); await _database.SaveChangesAsync(); + + string ibanGen = $"549745{StringExtensions.CreateRandom(12, "1234567890")}{account.Id:00000000}"; + + string cardNumberGen = $"49{StringExtensions.CreateRandom(6, "1234567890")}{account.Id:00000000}"; + byte[] cardNumber = _encryptionHelper.Encrypt(cardNumberGen); + + account.IBAN = ibanGen; + account.CardNumber = cardNumber; + + await _database.SaveChangesAsync(); + string password = GeneratePassword(); await GeneratePasswordVariants(password, account.Id); @@ -134,10 +193,9 @@ namespace SecureBank.API.Services //Send client code and temporary password to client by mail _logger.LogInformation($"INFO DIRECTLY TO CLIENT: Your client code is {account.Id:00000000}. Your temporary password is {password}. You will be prompted to change it at first login"); - return new APIResponse + return new APIResponse { - Data = account.Id, - Success = true + Data = account.Id }; } @@ -148,7 +206,7 @@ namespace SecureBank.API.Services { return new APIResponse { - Success = false, + Status = ResponseStatus.BadRequest, Message = $"Account does not exists" }; } @@ -157,7 +215,7 @@ namespace SecureBank.API.Services { return new APIResponse { - Success = false, + Status = ResponseStatus.BadRequest, Message = $"The number of failed login attempts for this account has exceeded 3. Contact your bank to confirm your identity and unlock your account." }; } @@ -166,7 +224,7 @@ namespace SecureBank.API.Services { return new APIResponse { - Success = false, + Status = ResponseStatus.BadRequest, Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account." }; } @@ -188,7 +246,6 @@ namespace SecureBank.API.Services return new APIResponse { - Success = true, Data = new GetPasswordVariantResponse { LoginRequestId = loginRequest.Id, @@ -198,26 +255,15 @@ namespace SecureBank.API.Services }; } - public async Task> Authentication(int accountId, AuthenticationRequest data) + public async Task> Authentication(AuthenticationRequest data) { - Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId); - - if (account is null) - { - return new APIResponse - { - Success = false, - Message = $"Account does not exists" - }; - } - AccountLoginRequest? loginRequest = await _database.AccountLoginRequests.FirstOrDefaultAsync(x => x.Id == data.LoginRequestId); if (loginRequest is null) { return new APIResponse { - Success = false, + Status = ResponseStatus.BadRequest, Message = $"Login request does not exist" }; } @@ -226,33 +272,17 @@ namespace SecureBank.API.Services Account loginRequestAccount = password.Account; - if (loginRequestAccount.Id != account.Id) + APIResponse? accountCheck = CheckAccount(loginRequestAccount); + if (accountCheck is not null) { - account.LockReason = "Suspicious login attempt. The account provided does not match the account to which the login request is assigned."; - loginRequestAccount.LockReason = "Suspicious login attempt. The account provided does not match the account to which the login request is assigned."; - await _database.SaveChangesAsync(); - - return new APIResponse - { - Success = false, - Message = $"Suspicious activity was detected during login. The account provided does not match the account to which the login request is assigned. Both accounts have been blocked. Contact your bank to confirm your identity and unlock your account." - }; - } - - if (account.LockReason is not null) - { - return new APIResponse - { - Success = false, - Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account." - }; + return accountCheck; } if (loginRequest.ValidTo < DateTime.Now) { return new APIResponse { - Success = false, + Status = ResponseStatus.BadRequest, ActionCode = 1, Message = $"Login request has expired. Go back and try again." }; @@ -261,25 +291,199 @@ namespace SecureBank.API.Services byte[] passwordDb = password.Password; byte[] passwordProvided = HashPassword(data.Password, password.LeftSalt, password.RightSalt); - if (Enumerable.SequenceEqual(passwordDb, passwordProvided)) + if (!Enumerable.SequenceEqual(passwordDb, passwordProvided)) { - account.LoginFailedCount++; + loginRequestAccount.LoginFailedCount++; await _database.SaveChangesAsync(); return new APIResponse { - Success = false, - ActionCode = 2, + Status = ResponseStatus.BadRequest, Message = $"Incorrect password" }; } - string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), account.Id, account.TemporaryPassword); + loginRequestAccount.LoginFailedCount = 0; + await _database.SaveChangesAsync(); + + string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), loginRequestAccount, loginRequestAccount.TemporaryPassword); + + return new APIResponse + { + ActionCode = loginRequestAccount.TemporaryPassword ? 2 : 0, + Data = token + }; + } + + public async Task> AuthenticationRefresh(Claims claims) + { + if (claims.IsOneTimeToken) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"One time token cannot be refreshed." + }; + } + + Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == claims.AccountId); + + APIResponse? accountCheck = CheckAccount(account); + if (accountCheck is not null) + { + return accountCheck; + } + + string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), account, false); return new APIResponse { - Data = token, - Success = true, + Data = token + }; + } + + public async Task ChangePassword(Claims claims, ChangePasswordRequest data) + { + string password = data.Password; + Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == claims.AccountId); + + if (account is null) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"Account does not exists" + }; + } + + IEnumerable passwordChecks = CheckPassword(password); + if (passwordChecks.Any()) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Provided password does not meet the security requirements:"); + foreach (string check in passwordChecks) + { + sb.AppendLine(check); + } + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = sb.ToString() + }; + } + + IEnumerable indexes = await _database.AccountPasswordIndexes.Where(x => x.AccountPassword.AccountId == claims.AccountId).ToListAsync(); + _database.AccountPasswordIndexes.AttachRange(indexes); + _database.AccountPasswordIndexes.RemoveRange(indexes); + await _database.SaveChangesAsync(); + + IEnumerable variants = await _database.AccountPasswords.Where(x => x.AccountId == claims.AccountId).ToListAsync(); + _database.AccountPasswords.AttachRange(variants); + _database.AccountPasswords.RemoveRange(variants); + await _database.SaveChangesAsync(); + + await GeneratePasswordVariants(password, claims.AccountId); + + account.TemporaryPassword = false; + await _database.SaveChangesAsync(); + + return new APIResponse(); + } + + public async Task>> GetAccounts(string? iban, int? id, Claims claims) + { + IEnumerable accounts = await _database.Accounts.ToListAsync(); + if (id is not null) + { + accounts = accounts.Where(x => x.Id == id); + } + if (iban is not null) + { + accounts = accounts.Where(x => x.IBAN == iban); + } + + if (accounts.Any(x => x.Id != claims.AccountId) && !claims.IsAdmin) + { + return new APIResponse> + { + Status = ResponseStatus.Unauthorized, + Message = $"You don't have permission to get information about accounts that aren't yours" + }; + } + + List data = new List(); + foreach (Account account in accounts) + { + data.Add(new AccountResponse + { + Id = account.Id, + FirstName = account.FirstName, + LastName = account.LastName, + Email = account.Email, + PhoneNumber = account.PhoneNumber, + Address = account.Address, + PESEL = _encryptionHelper.Decrypt(account.PESEL), + IdCardNumber = _encryptionHelper.Decrypt(account.IdCardNumber), + IBAN = account.IBAN, + CardNumber = _encryptionHelper.Decrypt(account.CardNumber), + CardExpirationDate = _encryptionHelper.Decrypt(account.CardExpirationDate), + CardCVV = _encryptionHelper.Decrypt(account.CardCVV), + IsAdmin = account.IsAdmin, + LoginFailedCount = account.LoginFailedCount, + TemporaryPassword = account.TemporaryPassword, + LockReason = account.LockReason, + }); + } + + return new APIResponse> + { + Data = data + }; + } + + public async Task ResetPassword(int accountId) + { + Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId); + + if (account is null) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"Account does not exists" + }; + } + + await PasswordReset(account); + + return new APIResponse + { + Data = account.Id + }; + } + + public async Task UnlockAccount(int accountId) + { + Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId); + + if (account is null) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"Account does not exists" + }; + } + + await PasswordReset(account); + + account.LockReason = null; + account.LoginFailedCount = 0; + await _database.SaveChangesAsync(); + + return new APIResponse + { + Data = account.Id }; } @@ -289,6 +493,60 @@ namespace SecureBank.API.Services #region PRIVATE METHODS + protected async Task PasswordReset(Account account) + { + IEnumerable indexes = await _database.AccountPasswordIndexes.Where(x => x.AccountPassword.AccountId == account.Id).ToListAsync(); + _database.AccountPasswordIndexes.AttachRange(indexes); + _database.AccountPasswordIndexes.RemoveRange(indexes); + await _database.SaveChangesAsync(); + + IEnumerable variants = await _database.AccountPasswords.Where(x => x.AccountId == account.Id).ToListAsync(); + _database.AccountPasswords.AttachRange(variants); + _database.AccountPasswords.RemoveRange(variants); + await _database.SaveChangesAsync(); + + string password = GeneratePassword(); + + await GeneratePasswordVariants(password, account.Id); + + account.TemporaryPassword = true; + await _database.SaveChangesAsync(); + + _logger.LogInformation($"INFO DIRECTLY TO CLIENT: Your new temporary password is {password}. You will be prompted to change it at first login"); + } + + protected APIResponse? CheckAccount(Account? account) + { + if (account is null) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"Account does not exists." + }; + } + + if (account.LockReason is not null) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account." + }; + } + + if (account.LoginFailedCount >= 3) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"The number of failed login attempts for this account has exceeded 3. Contact your bank to confirm your identity and unlock your account." + }; + } + + return null; + } + protected byte[] HashPassword(string password, string leftSalt, string rightSalt) { SHA512 sha = SHA512.Create(); @@ -355,11 +613,16 @@ namespace SecureBank.API.Services protected IEnumerable CheckPassword(string password) { int minLength = 8; + uint maxLength = 20; if (password.Length < minLength) { yield return $"Password must be at least {minLength} characters long"; } + if (password.Length > maxLength) + { + yield return $"Password cannot be longer than {maxLength} characters"; + } if (!password.Any(x => Char.IsUpper(x))) { yield return $"Password must contain at least one uppercase character"; @@ -372,7 +635,7 @@ namespace SecureBank.API.Services { yield return $"Password must contain at least one digit"; } - if (!password.Any(x => Char.IsSymbol(x))) + if (!password.Any(x => !Char.IsDigit(x) && !Char.IsUpper(x) && !Char.IsLower(x))) { yield return $"Password must contain at least one special character"; } diff --git a/SecureBank.API/SecureBank.API.Services/BalanceService.cs b/SecureBank.API/SecureBank.API.Services/BalanceService.cs new file mode 100644 index 0000000..bf3e3ae --- /dev/null +++ b/SecureBank.API/SecureBank.API.Services/BalanceService.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.EntityFrameworkCore; +using SecureBank.Authentication; +using SecureBank.Common; +using SecureBank.Database; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.API.Services +{ + public interface IBalanceService + { + Task> GetAccountBalance(int accountId); + Task> GetBalance(Claims claims); + } + + + + public class BalanceService : IBalanceService + { + #region SERVICES + + private DatabaseContext _database; + + #endregion + + + + #region CONSTRUCTORS + + public BalanceService(DatabaseContext database) + { + _database = database; + } + + #endregion + + + + #region PUBLIC METHODS + + public async Task> GetBalance(Claims claims) => await GetAccountBalance(claims.AccountId); + + public async Task> GetAccountBalance(int accountId) + { + Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId); + + if (account is null) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = "Account does not exists" + }; + } + + string iban = account.IBAN; + + Transfer[] transfersIncoming = await _database.Transfers.Where(x => x.ReceiverAccountNumber == iban).ToArrayAsync(); + Transfer[] transfersOutcoming = await _database.Transfers.Where(x => x.SenderAccountNumber == iban).ToArrayAsync(); + + return new APIResponse + { + Data = 0 + transfersIncoming.Sum(x => x.Amount) - transfersOutcoming.Sum(x => x.Amount), + }; + } + + #endregion + + + + #region PRIVATE METHODS + + + + #endregion + } +} diff --git a/SecureBank.API/SecureBank.API.Services/SecureBank.API.Services.csproj b/SecureBank.API/SecureBank.API.Services/SecureBank.API.Services.csproj index 479dbfa..56b0b81 100644 --- a/SecureBank.API/SecureBank.API.Services/SecureBank.API.Services.csproj +++ b/SecureBank.API/SecureBank.API.Services/SecureBank.API.Services.csproj @@ -7,10 +7,16 @@ + + + + + + diff --git a/SecureBank.API/SecureBank.API.Services/TransfersService.cs b/SecureBank.API/SecureBank.API.Services/TransfersService.cs new file mode 100644 index 0000000..a6ee0b9 --- /dev/null +++ b/SecureBank.API/SecureBank.API.Services/TransfersService.cs @@ -0,0 +1,250 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Identity.Client; +using SecureBank.API.Helpers; +using SecureBank.Authentication; +using SecureBank.Common; +using SecureBank.Common.Accounts; +using SecureBank.Common.Transfers; +using SecureBank.Database; +using SecureBank.Helpers.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.API.Services +{ + public interface ITransfersService + { + Task>> GetTransfers(Claims claims); + Task>> GetUserTransfers(int accountId); + Task CreateAdminTransfer(CreateAdminTransferRequest data); + Task CreateUserTransfer(CreateUserTransferRequest data, Claims claims); + } + + + + + public class TransfersService : ITransfersService + { + #region SERVICES + + private DatabaseContext _database; + + #endregion + + + + #region CONSTRUCTORS + + public TransfersService(DatabaseContext database) + { + _database = database; + } + + #endregion + + + + #region PUBLIC METHODS + + public async Task>> GetTransfers(Claims claims) => await GetUserTransfers(claims.AccountId); + public async Task>> GetUserTransfers(int accountId) + { + Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId); + + if (account is null) + { + return new APIResponse> + { + Status = ResponseStatus.BadRequest, + Message = "Account does not exists" + }; + } + + string iban = account.IBAN; + + List list = new List(); + await foreach (Transfer transfer in _database.Transfers.Where(x => x.SenderAccountNumber == iban || x.ReceiverAccountNumber == iban).AsAsyncEnumerable()) + { + list.Add(new TransferResponse + { + Id = transfer.Id, + SenderAccountNumber = transfer.SenderAccountNumber, + SenderAddress = transfer.SenderAddress, + SenderName = transfer.SenderName, + ReceiverAccountNumber = transfer.ReceiverAccountNumber, + ReceiverAddress = transfer.ReceiverAddress, + ReceiverName = transfer.ReceiverName, + Amount = transfer.Amount, + Title = transfer.Title, + Date = transfer.Date, + }); + } + + return new APIResponse> + { + Data = list + }; + } + + public async Task CreateAdminTransfer(CreateAdminTransferRequest data) + { + Check[] checks = new Check[] + { + new Check + { + CheckAction = new Predicate((x) => x is null), + Message = "Body cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => x.SenderAccountNumber is null), + Message = "Sender account number cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => !x.SenderAccountNumber.All(y => char.IsDigit(y))), + Message = "Wrong sender account number format. Account number consists only of digits" + }, + new Check + { + CheckAction = new Predicate((x) => x.SenderAccountNumber.Length != 26), + Message = "Sender account number cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => x.ReceiverAccountNumber is null), + Message = "Receiver account number cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => !x.ReceiverAccountNumber.All(y => char.IsDigit(y))), + Message = "Wrong receiver account number format. Account number consists only of digits" + }, + new Check + { + CheckAction = new Predicate((x) => x.ReceiverAccountNumber.Length != 26), + Message = "Receiver account number cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => x.Amount <= 0), + Message = "Receiver account number cannot be empty" + }, + }; + + foreach (Check check in checks) + { + if (check.CheckAction.Invoke(data)) + { + return new APIResponse + { + Message = check.Message, + Status = ResponseStatus.BadRequest, + }; + } + } + + data.Amount = Math.Round(data.Amount, 2, MidpointRounding.ToEven); + + Transfer transfer = new Transfer + { + SenderAccountNumber = data.SenderAccountNumber, + SenderAddress = data.SenderAddress, + SenderName = data.SenderName, + ReceiverAccountNumber = data.ReceiverAccountNumber, + ReceiverAddress = data.ReceiverAddress, + ReceiverName = data.ReceiverName, + Amount = data.Amount, + Title = data.Title, + Date = DateTime.Now, + }; + await _database.Transfers.AddAsync(transfer); + await _database.SaveChangesAsync(); + + return new APIResponse(); + } + + public async Task CreateUserTransfer(CreateUserTransferRequest data, Claims claims) + { + Check[] checks = new Check[] + { + new Check + { + CheckAction = new Predicate((x) => x is null), + Message = "Body cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => x.ReceiverAccountNumber is null), + Message = "Receiver account number cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => !x.ReceiverAccountNumber.All(y => char.IsDigit(y))), + Message = "Wrong receiver account number format. Account number consists only of digits" + }, + new Check + { + CheckAction = new Predicate((x) => x.ReceiverAccountNumber.Length != 26), + Message = "Receiver account number cannot be empty" + }, + new Check + { + CheckAction = new Predicate((x) => x.Amount <= 0), + Message = "Receiver account number cannot be empty" + }, + }; + + foreach (Check check in checks) + { + if (check.CheckAction.Invoke(data)) + { + return new APIResponse + { + Message = check.Message, + Status = ResponseStatus.BadRequest, + }; + } + } + + Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == claims.AccountId); + + if (account is null) + { + return new APIResponse> + { + Status = ResponseStatus.BadRequest, + Message = "Account does not exists" + }; + } + + data.Amount = Math.Round(data.Amount, 2, MidpointRounding.ToEven); + + Transfer transfer = new Transfer + { + SenderAccountNumber = account.IBAN, + SenderAddress = account.Address, + SenderName = $"{account.FirstName} {account.LastName}", + ReceiverAccountNumber = data.ReceiverAccountNumber, + ReceiverAddress = data.ReceiverAddress, + ReceiverName = data.ReceiverName, + Amount = data.Amount, + Title = data.Title, + Date = DateTime.Now, + }; + await _database.Transfers.AddAsync(transfer); + await _database.SaveChangesAsync(); + + return new APIResponse(); + } + + #endregion + } +} diff --git a/SecureBank.Authentication/Claims.cs b/SecureBank.Authentication/Claims.cs new file mode 100644 index 0000000..8bc05cd --- /dev/null +++ b/SecureBank.Authentication/Claims.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.Authentication +{ + public class Claims + { + #region PROPERTIES + + public Guid Id { get; protected set; } + public int AccountId { get; protected set; } + public string FirstName { get; protected set; } + public string LastName { get; protected set; } + public DateTime ExpirationTime { get; protected set; } + public bool IsOneTimeToken { get; protected set; } + public bool IsAdmin { get; protected set; } + + #endregion + + + + #region CONSTRUCTORS + + public Claims(IEnumerable claims) + { + Id = Guid.Parse(claims.Where(x => x.Type == "jti").First().Value); + AccountId = int.Parse(claims.Where(x => x.Type == "uid").First().Value); + FirstName = claims.Where(x => x.Type == "first_name").First().Value; + LastName = claims.Where(x => x.Type == "last_name").First().Value; + ExpirationTime = new DateTime(long.Parse(claims.Where(x => x.Type == "exp").First().Value)); + IsOneTimeToken = bool.Parse(claims.Where(x => x.Type == "one_time_token").First().Value); + IsAdmin = bool.Parse(claims.Where(x => x.Type == "admin").First().Value); + } + + #endregion + } +} diff --git a/SecureBank.Helpers/SecureBank.Helpers.csproj b/SecureBank.Authentication/SecureBank.Authentication.csproj similarity index 100% rename from SecureBank.Helpers/SecureBank.Helpers.csproj rename to SecureBank.Authentication/SecureBank.Authentication.csproj diff --git a/SecureBank.Common/APIResponse.cs b/SecureBank.Common/APIResponse.cs index 7a8d53b..660cf7f 100644 --- a/SecureBank.Common/APIResponse.cs +++ b/SecureBank.Common/APIResponse.cs @@ -12,21 +12,21 @@ namespace SecureBank.Common { [JsonProperty("message")] [JsonPropertyName("message")] - public string Message { get; set; } + public string? Message { get; set; } - [JsonProperty("success")] - [JsonPropertyName("success")] - public bool Success { get; set; } + [JsonProperty("status")] + [JsonPropertyName("status")] + public ResponseStatus Status { get; set; } = ResponseStatus.Ok; [JsonProperty("action_code")] [JsonPropertyName("action_code")] - public int ActionCode { get; set; } + public int? ActionCode { get; set; } } public class APIResponse : APIResponse { [JsonProperty("data")] [JsonPropertyName("data")] - public T Data { get; set; } + public T? Data { get; set; } } } diff --git a/SecureBank.Common/Accounts/AccountResponse.cs b/SecureBank.Common/Accounts/AccountResponse.cs new file mode 100644 index 0000000..53e9623 --- /dev/null +++ b/SecureBank.Common/Accounts/AccountResponse.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace SecureBank.Common.Accounts +{ + public class AccountResponse + { + [JsonProperty("id")] + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonProperty("first_name")] + [JsonPropertyName("first_name")] + public string FirstName { get; set; } + + [JsonProperty("last_name")] + [JsonPropertyName("last_name")] + public string LastName { get; set; } + + [JsonProperty("email")] + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonProperty("phone_number")] + [JsonPropertyName("phone_number")] + public string PhoneNumber { get; set; } + + [JsonProperty("address")] + [JsonPropertyName("address")] + public string Address { get; set; } + + [JsonProperty("pesel")] + [JsonPropertyName("pesel")] + public string PESEL { get; set; } + + [JsonProperty("id_card_number")] + [JsonPropertyName("id_card_number")] + public string IdCardNumber { get; set; } + + [JsonProperty("iban")] + [JsonPropertyName("iban")] + public string IBAN { get; set; } + + [JsonProperty("card_number")] + [JsonPropertyName("card_number")] + public string CardNumber { get; set; } + + [JsonProperty("card_expiration_date")] + [JsonPropertyName("card_expiration_date")] + public string CardExpirationDate { get; set; } + + [JsonProperty("card_cvv")] + [JsonPropertyName("card_cvv")] + public string CardCVV { get; set; } + + [JsonProperty("is_admin")] + [JsonPropertyName("is_admin")] + public bool IsAdmin { get; set; } + + [JsonProperty("login_failed_count")] + [JsonPropertyName("login_failed_count")] + public int LoginFailedCount { get; set; } + + [JsonProperty("temporary_password")] + [JsonPropertyName("temporary_password")] + public bool TemporaryPassword { get; set; } + + [JsonProperty("lock_reason")] + [JsonPropertyName("lock_reason")] + public string? LockReason { get; set; } + } +} diff --git a/SecureBank.Common/Accounts/ChangePasswordRequest.cs b/SecureBank.Common/Accounts/ChangePasswordRequest.cs new file mode 100644 index 0000000..b0914b8 --- /dev/null +++ b/SecureBank.Common/Accounts/ChangePasswordRequest.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace SecureBank.Common.Accounts +{ + public class ChangePasswordRequest + { + [JsonProperty("password")] + [JsonPropertyName("password")] + public string Password { get; set; } + } +} diff --git a/SecureBank.Common/Accounts/CreateAccountRequest.cs b/SecureBank.Common/Accounts/CreateAccountRequest.cs index 04a0ac4..47b5056 100644 --- a/SecureBank.Common/Accounts/CreateAccountRequest.cs +++ b/SecureBank.Common/Accounts/CreateAccountRequest.cs @@ -25,5 +25,17 @@ namespace SecureBank.Common.Accounts [JsonProperty("phone_number")] [JsonPropertyName("phone_number")] public string PhoneNumber { get; set; } + + [JsonProperty("address")] + [JsonPropertyName("address")] + public string Address { get; set; } + + [JsonProperty("pesel")] + [JsonPropertyName("pesel")] + public string PESEL { get; set; } + + [JsonProperty("id_card_number")] + [JsonPropertyName("id_card_number")] + public string IdCardNumber { get; set; } } } diff --git a/SecureBank.Common/ResponseStatus.cs b/SecureBank.Common/ResponseStatus.cs new file mode 100644 index 0000000..541d5a3 --- /dev/null +++ b/SecureBank.Common/ResponseStatus.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.Common +{ + public enum ResponseStatus + { + Ok, + Unauthorized, + BadRequest + } +} diff --git a/SecureBank.Common/Transfers/CreateAdminTransferRequest.cs b/SecureBank.Common/Transfers/CreateAdminTransferRequest.cs new file mode 100644 index 0000000..b2429da --- /dev/null +++ b/SecureBank.Common/Transfers/CreateAdminTransferRequest.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace SecureBank.Common.Transfers +{ + public class CreateAdminTransferRequest + { + [JsonProperty("sender_account_number")] + [JsonPropertyName("sender_account_number")] + public string SenderAccountNumber { get; set; } + + [JsonProperty("sender_name")] + [JsonPropertyName("sender_name")] + public string? SenderName { get; set; } + + [JsonProperty("sender_address")] + [JsonPropertyName("sender_address")] + public string? SenderAddress { get; set; } + + [JsonProperty("receiver_account_number")] + [JsonPropertyName("receiver_account_number")] + public string ReceiverAccountNumber { get; set; } + + [JsonProperty("receiver_name")] + [JsonPropertyName("receiver_name")] + public string? ReceiverName { get; set; } + + [JsonProperty("receiver_address")] + [JsonPropertyName("receiver_address")] + public string? ReceiverAddress { get; set; } + + [JsonProperty("title")] + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonProperty("amount")] + [JsonPropertyName("amount")] + public decimal Amount { get; set; } + } +} diff --git a/SecureBank.Common/Transfers/CreateUserTransferRequest.cs b/SecureBank.Common/Transfers/CreateUserTransferRequest.cs new file mode 100644 index 0000000..e876cc9 --- /dev/null +++ b/SecureBank.Common/Transfers/CreateUserTransferRequest.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace SecureBank.Common.Transfers +{ + public class CreateUserTransferRequest + { + + [JsonProperty("receiver_account_number")] + [JsonPropertyName("receiver_account_number")] + public string ReceiverAccountNumber { get; set; } + + [JsonProperty("receiver_name")] + [JsonPropertyName("receiver_name")] + public string? ReceiverName { get; set; } + + [JsonProperty("receiver_address")] + [JsonPropertyName("receiver_address")] + public string? ReceiverAddress { get; set; } + + [JsonProperty("title")] + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonProperty("amount")] + [JsonPropertyName("amount")] + public decimal Amount { get; set; } + } +} diff --git a/SecureBank.Common/Transfers/TransferResponse.cs b/SecureBank.Common/Transfers/TransferResponse.cs new file mode 100644 index 0000000..d637abe --- /dev/null +++ b/SecureBank.Common/Transfers/TransferResponse.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace SecureBank.Common.Transfers +{ + public class TransferResponse + { + [JsonProperty("id")] + [JsonPropertyName("id")] + public Guid Id { get; set; } + + [JsonProperty("sender_account_number")] + [JsonPropertyName("sender_account_number")] + public string SenderAccountNumber { get; set; } + + [JsonProperty("sender_name")] + [JsonPropertyName("sender_name")] + public string? SenderName { get; set; } + + [JsonProperty("sender_address")] + [JsonPropertyName("sender_address")] + public string? SenderAddress { get; set; } + + [JsonProperty("receiver_account_number")] + [JsonPropertyName("receiver_account_number")] + public string ReceiverAccountNumber { get; set; } + + [JsonProperty("receiver_name")] + [JsonPropertyName("receiver_name")] + public string? ReceiverName { get; set; } + + [JsonProperty("receiver_address")] + [JsonPropertyName("receiver_address")] + public string? ReceiverAddress { get; set; } + + [JsonProperty("amount")] + [JsonPropertyName("amount")] + public decimal Amount { get; set; } + + [JsonProperty("title")] + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonProperty("date")] + [JsonPropertyName("date")] + public DateTime Date { get; set; } + } +} diff --git a/SecureBank.Database/Account.cs b/SecureBank.Database/Account.cs index 7685436..35f9e51 100644 --- a/SecureBank.Database/Account.cs +++ b/SecureBank.Database/Account.cs @@ -30,6 +30,37 @@ namespace SecureBank.Database [MaxLength(20)] public string PhoneNumber { get; set; } + [Required] + [MaxLength(500)] + public string Address { get; set; } + + [Required] + [MaxLength(16)] + public byte[] PESEL { get; set; } + + [Required] + [MaxLength(16)] + public byte[] IdCardNumber { get; set; } + + [Required] + [MaxLength(26)] + public string IBAN { get; set; } + + [Required] + [MaxLength(32)] + public byte[] CardNumber { get; set; } + + [Required] + [MaxLength(16)] + public byte[] CardExpirationDate { get; set; } + + [Required] + [MaxLength(16)] + public byte[] CardCVV { get; set; } + + [Required] + public bool IsAdmin { get; set; } = false; + [Required] public byte LoginFailedCount { get; set; } = 0; diff --git a/SecureBank.Database/DatabaseContext.cs b/SecureBank.Database/DatabaseContext.cs index e99fe03..55d1273 100644 --- a/SecureBank.Database/DatabaseContext.cs +++ b/SecureBank.Database/DatabaseContext.cs @@ -25,6 +25,7 @@ namespace SecureBank.Database public virtual DbSet AccountPasswords { get; set; } public virtual DbSet AccountPasswordIndexes { get; set; } public virtual DbSet AccountLoginRequests { get; set; } + public virtual DbSet Transfers { get; set; } #endregion diff --git a/SecureBank.Database/Migrations/20240114165546_Migration1.Designer.cs b/SecureBank.Database/Migrations/20240114165546_Migration1.Designer.cs deleted file mode 100644 index 58b68ce..0000000 --- a/SecureBank.Database/Migrations/20240114165546_Migration1.Designer.cs +++ /dev/null @@ -1,132 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SecureBank.Database; - -#nullable disable - -namespace SecureBank.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20240114165546_Migration1")] - partial class Migration1 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("SecureBank.Database.Account", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("TEXT"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LoginFailedCount") - .HasColumnType("INTEGER"); - - b.Property("PhoneNumber") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.Property("TemporaryPassword") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPassword", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccountId") - .HasColumnType("INTEGER"); - - b.Property("LeftSalt") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("BLOB"); - - b.Property("RightSalt") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccountId"); - - b.ToTable("AccountPasswords"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b => - { - b.Property("AccountPasswordId") - .HasColumnType("INTEGER"); - - b.Property("Index") - .HasColumnType("INTEGER"); - - b.HasIndex("AccountPasswordId"); - - b.ToTable("AccountPasswordIndexes"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPassword", b => - { - b.HasOne("SecureBank.Database.Account", "Account") - .WithMany("AccountPasswords") - .HasForeignKey("AccountId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Account"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b => - { - b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") - .WithMany() - .HasForeignKey("AccountPasswordId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AccountPassword"); - }); - - modelBuilder.Entity("SecureBank.Database.Account", b => - { - b.Navigation("AccountPasswords"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SecureBank.Database/Migrations/20240114170227_Migration2.Designer.cs b/SecureBank.Database/Migrations/20240114170227_Migration2.Designer.cs deleted file mode 100644 index 125d37e..0000000 --- a/SecureBank.Database/Migrations/20240114170227_Migration2.Designer.cs +++ /dev/null @@ -1,138 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SecureBank.Database; - -#nullable disable - -namespace SecureBank.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20240114170227_Migration2")] - partial class Migration2 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("SecureBank.Database.Account", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("TEXT"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LoginFailedCount") - .HasColumnType("INTEGER"); - - b.Property("PhoneNumber") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.Property("TemporaryPassword") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPassword", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccountId") - .HasColumnType("INTEGER"); - - b.Property("LeftSalt") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("BLOB"); - - b.Property("RightSalt") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccountId"); - - b.ToTable("AccountPasswords"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccountPasswordId") - .HasColumnType("INTEGER"); - - b.Property("Index") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AccountPasswordId"); - - b.ToTable("AccountPasswordIndexes"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPassword", b => - { - b.HasOne("SecureBank.Database.Account", "Account") - .WithMany("AccountPasswords") - .HasForeignKey("AccountId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Account"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b => - { - b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") - .WithMany() - .HasForeignKey("AccountPasswordId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AccountPassword"); - }); - - modelBuilder.Entity("SecureBank.Database.Account", b => - { - b.Navigation("AccountPasswords"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SecureBank.Database/Migrations/20240114170227_Migration2.cs b/SecureBank.Database/Migrations/20240114170227_Migration2.cs deleted file mode 100644 index 0aa52ab..0000000 --- a/SecureBank.Database/Migrations/20240114170227_Migration2.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace SecureBank.Database.Migrations -{ - /// - public partial class Migration2 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Id", - table: "AccountPasswordIndexes", - type: "INTEGER", - nullable: false, - defaultValue: 0L) - .Annotation("Sqlite:Autoincrement", true); - - migrationBuilder.AddPrimaryKey( - name: "PK_AccountPasswordIndexes", - table: "AccountPasswordIndexes", - column: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_AccountPasswordIndexes", - table: "AccountPasswordIndexes"); - - migrationBuilder.DropColumn( - name: "Id", - table: "AccountPasswordIndexes"); - } - } -} diff --git a/SecureBank.Database/Migrations/20240115084527_Migration3.Designer.cs b/SecureBank.Database/Migrations/20240115084527_Migration3.Designer.cs deleted file mode 100644 index 1567d73..0000000 --- a/SecureBank.Database/Migrations/20240115084527_Migration3.Designer.cs +++ /dev/null @@ -1,169 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SecureBank.Database; - -#nullable disable - -namespace SecureBank.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20240115084527_Migration3")] - partial class Migration3 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("SecureBank.Database.Account", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("TEXT"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LoginFailedCount") - .HasColumnType("INTEGER"); - - b.Property("PhoneNumber") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.Property("TemporaryPassword") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("AccountPasswordId") - .HasColumnType("INTEGER"); - - b.Property("ValidTo") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccountPasswordId"); - - b.ToTable("AccountLoginRequests"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPassword", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccountId") - .HasColumnType("INTEGER"); - - b.Property("LeftSalt") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("BLOB"); - - b.Property("RightSalt") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccountId"); - - b.ToTable("AccountPasswords"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccountPasswordId") - .HasColumnType("INTEGER"); - - b.Property("Index") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AccountPasswordId"); - - b.ToTable("AccountPasswordIndexes"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b => - { - b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") - .WithMany() - .HasForeignKey("AccountPasswordId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AccountPassword"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPassword", b => - { - b.HasOne("SecureBank.Database.Account", "Account") - .WithMany("AccountPasswords") - .HasForeignKey("AccountId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Account"); - }); - - modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b => - { - b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") - .WithMany() - .HasForeignKey("AccountPasswordId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AccountPassword"); - }); - - modelBuilder.Entity("SecureBank.Database.Account", b => - { - b.Navigation("AccountPasswords"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SecureBank.Database/Migrations/20240115084527_Migration3.cs b/SecureBank.Database/Migrations/20240115084527_Migration3.cs deleted file mode 100644 index bf90885..0000000 --- a/SecureBank.Database/Migrations/20240115084527_Migration3.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace SecureBank.Database.Migrations -{ - /// - public partial class Migration3 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AccountLoginRequests", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - AccountPasswordId = table.Column(type: "INTEGER", nullable: false), - ValidTo = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AccountLoginRequests", x => x.Id); - table.ForeignKey( - name: "FK_AccountLoginRequests_AccountPasswords_AccountPasswordId", - column: x => x.AccountPasswordId, - principalTable: "AccountPasswords", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AccountLoginRequests_AccountPasswordId", - table: "AccountLoginRequests", - column: "AccountPasswordId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AccountLoginRequests"); - } - } -} diff --git a/SecureBank.Database/Migrations/20240115132220_Migration4.cs b/SecureBank.Database/Migrations/20240115132220_Migration4.cs deleted file mode 100644 index 57cc3a2..0000000 --- a/SecureBank.Database/Migrations/20240115132220_Migration4.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace SecureBank.Database.Migrations -{ - /// - public partial class Migration4 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LockReason", - table: "Accounts", - type: "TEXT", - maxLength: 1000, - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LockReason", - table: "Accounts"); - } - } -} diff --git a/SecureBank.Database/Migrations/20240115132220_Migration4.Designer.cs b/SecureBank.Database/Migrations/20240122225115_Migration1.Designer.cs similarity index 64% rename from SecureBank.Database/Migrations/20240115132220_Migration4.Designer.cs rename to SecureBank.Database/Migrations/20240122225115_Migration1.Designer.cs index 2a8543a..c23f166 100644 --- a/SecureBank.Database/Migrations/20240115132220_Migration4.Designer.cs +++ b/SecureBank.Database/Migrations/20240122225115_Migration1.Designer.cs @@ -11,8 +11,8 @@ using SecureBank.Database; namespace SecureBank.Database.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20240115132220_Migration4")] - partial class Migration4 + [Migration("20240122225115_Migration1")] + partial class Migration1 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -26,6 +26,26 @@ namespace SecureBank.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Address") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CardCVV") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + + b.Property("CardExpirationDate") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + + b.Property("CardNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("BLOB"); + b.Property("Email") .IsRequired() .HasMaxLength(300) @@ -36,6 +56,19 @@ namespace SecureBank.Database.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("IBAN") + .IsRequired() + .HasMaxLength(26) + .HasColumnType("TEXT"); + + b.Property("IdCardNumber") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + b.Property("LastName") .IsRequired() .HasMaxLength(100) @@ -48,6 +81,11 @@ namespace SecureBank.Database.Migrations b.Property("LoginFailedCount") .HasColumnType("INTEGER"); + b.Property("PESEL") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + b.Property("PhoneNumber") .IsRequired() .HasMaxLength(20) @@ -130,6 +168,55 @@ namespace SecureBank.Database.Migrations b.ToTable("AccountPasswordIndexes"); }); + modelBuilder.Entity("SecureBank.Database.Transfer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(14, 2) + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("ReceiverAccountNumber") + .IsRequired() + .HasMaxLength(26) + .HasColumnType("TEXT"); + + b.Property("ReceiverAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ReceiverName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SenderAccountNumber") + .IsRequired() + .HasMaxLength(26) + .HasColumnType("TEXT"); + + b.Property("SenderAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SenderName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Transfers"); + }); + modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b => { b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") diff --git a/SecureBank.Database/Migrations/20240114165546_Migration1.cs b/SecureBank.Database/Migrations/20240122225115_Migration1.cs similarity index 51% rename from SecureBank.Database/Migrations/20240114165546_Migration1.cs rename to SecureBank.Database/Migrations/20240122225115_Migration1.cs index a315a11..661c0f8 100644 --- a/SecureBank.Database/Migrations/20240114165546_Migration1.cs +++ b/SecureBank.Database/Migrations/20240122225115_Migration1.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -20,14 +21,43 @@ namespace SecureBank.Database.Migrations LastName = table.Column(type: "TEXT", maxLength: 100, nullable: false), Email = table.Column(type: "TEXT", maxLength: 300, nullable: false), PhoneNumber = table.Column(type: "TEXT", maxLength: 20, nullable: false), + Address = table.Column(type: "TEXT", maxLength: 500, nullable: false), + PESEL = table.Column(type: "BLOB", maxLength: 16, nullable: false), + IdCardNumber = table.Column(type: "BLOB", maxLength: 16, nullable: false), + IBAN = table.Column(type: "TEXT", maxLength: 26, nullable: false), + CardNumber = table.Column(type: "BLOB", maxLength: 32, nullable: false), + CardExpirationDate = table.Column(type: "BLOB", maxLength: 16, nullable: false), + CardCVV = table.Column(type: "BLOB", maxLength: 16, nullable: false), + IsAdmin = table.Column(type: "INTEGER", nullable: false), LoginFailedCount = table.Column(type: "INTEGER", nullable: false), - TemporaryPassword = table.Column(type: "INTEGER", nullable: false) + TemporaryPassword = table.Column(type: "INTEGER", nullable: false), + LockReason = table.Column(type: "TEXT", maxLength: 1000, nullable: true) }, constraints: table => { table.PrimaryKey("PK_Accounts", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Transfers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + SenderAccountNumber = table.Column(type: "TEXT", maxLength: 26, nullable: false), + SenderName = table.Column(type: "TEXT", maxLength: 100, nullable: true), + SenderAddress = table.Column(type: "TEXT", maxLength: 100, nullable: true), + ReceiverAccountNumber = table.Column(type: "TEXT", maxLength: 26, nullable: false), + ReceiverName = table.Column(type: "TEXT", maxLength: 100, nullable: true), + ReceiverAddress = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Amount = table.Column(type: "TEXT", precision: 14, scale: 2, nullable: false), + Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Date = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Transfers", x => x.Id); + }); + migrationBuilder.CreateTable( name: "AccountPasswords", columns: table => new @@ -50,15 +80,37 @@ namespace SecureBank.Database.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "AccountLoginRequests", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountPasswordId = table.Column(type: "INTEGER", nullable: false), + ValidTo = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountLoginRequests", x => x.Id); + table.ForeignKey( + name: "FK_AccountLoginRequests_AccountPasswords_AccountPasswordId", + column: x => x.AccountPasswordId, + principalTable: "AccountPasswords", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "AccountPasswordIndexes", columns: table => new { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), AccountPasswordId = table.Column(type: "INTEGER", nullable: false), Index = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { + table.PrimaryKey("PK_AccountPasswordIndexes", x => x.Id); table.ForeignKey( name: "FK_AccountPasswordIndexes_AccountPasswords_AccountPasswordId", column: x => x.AccountPasswordId, @@ -67,6 +119,11 @@ namespace SecureBank.Database.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateIndex( + name: "IX_AccountLoginRequests_AccountPasswordId", + table: "AccountLoginRequests", + column: "AccountPasswordId"); + migrationBuilder.CreateIndex( name: "IX_AccountPasswordIndexes_AccountPasswordId", table: "AccountPasswordIndexes", @@ -81,9 +138,15 @@ namespace SecureBank.Database.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "AccountLoginRequests"); + migrationBuilder.DropTable( name: "AccountPasswordIndexes"); + migrationBuilder.DropTable( + name: "Transfers"); + migrationBuilder.DropTable( name: "AccountPasswords"); diff --git a/SecureBank.Database/Migrations/DatabaseContextModelSnapshot.cs b/SecureBank.Database/Migrations/DatabaseContextModelSnapshot.cs index bc0b3d4..0ad7695 100644 --- a/SecureBank.Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/SecureBank.Database/Migrations/DatabaseContextModelSnapshot.cs @@ -23,6 +23,26 @@ namespace SecureBank.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Address") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CardCVV") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + + b.Property("CardExpirationDate") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + + b.Property("CardNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("BLOB"); + b.Property("Email") .IsRequired() .HasMaxLength(300) @@ -33,6 +53,19 @@ namespace SecureBank.Database.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("IBAN") + .IsRequired() + .HasMaxLength(26) + .HasColumnType("TEXT"); + + b.Property("IdCardNumber") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + b.Property("LastName") .IsRequired() .HasMaxLength(100) @@ -45,6 +78,11 @@ namespace SecureBank.Database.Migrations b.Property("LoginFailedCount") .HasColumnType("INTEGER"); + b.Property("PESEL") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("BLOB"); + b.Property("PhoneNumber") .IsRequired() .HasMaxLength(20) @@ -127,6 +165,55 @@ namespace SecureBank.Database.Migrations b.ToTable("AccountPasswordIndexes"); }); + modelBuilder.Entity("SecureBank.Database.Transfer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(14, 2) + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("ReceiverAccountNumber") + .IsRequired() + .HasMaxLength(26) + .HasColumnType("TEXT"); + + b.Property("ReceiverAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ReceiverName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SenderAccountNumber") + .IsRequired() + .HasMaxLength(26) + .HasColumnType("TEXT"); + + b.Property("SenderAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SenderName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Transfers"); + }); + modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b => { b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") diff --git a/SecureBank.Database/Transfer.cs b/SecureBank.Database/Transfer.cs new file mode 100644 index 0000000..dfb3893 --- /dev/null +++ b/SecureBank.Database/Transfer.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.Database +{ + public partial class Transfer + { + #region PROPERTIES + + [Key] + public Guid Id { get; set; } + + [Required] + [MaxLength(26)] + public string SenderAccountNumber { get; set; } + + [MaxLength(100)] + public string? SenderName { get; set; } + + [MaxLength(100)] + public string? SenderAddress { get; set; } + + [Required] + [MaxLength(26)] + public string ReceiverAccountNumber { get; set; } + + [MaxLength(100)] + public string? ReceiverName { get; set; } + + [MaxLength(100)] + public string? ReceiverAddress { get; set; } + + [Required] + [Precision(14, 2)] + public decimal Amount { get; set; } + + [Required] + [MaxLength(200)] + public string? Title { get; set; } + + [Required] + public DateTime Date { get; set; } + + #endregion + } +} diff --git a/SecureBank.Helpers/AuthenticationHelper.cs b/SecureBank.Helpers/AuthenticationHelper.cs deleted file mode 100644 index 36f70e6..0000000 --- a/SecureBank.Helpers/AuthenticationHelper.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace SecureBank.Helpers -{ - public class AuthenticationHelper - { - #region METHODS - - public static IEnumerable ParseToken(string token) - { - string payload = token.Split('.')[1]; - - switch (payload.Length % 4) - { - case 2: payload += "=="; break; - case 3: payload += "="; break; - } - - byte[] jsonBytes = Convert.FromBase64String(payload); - Dictionary? keyValuePairs = JsonSerializer.Deserialize>(jsonBytes); - - if (keyValuePairs is null) - { - throw new Exception("Incorrect token"); - } - - return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())); - } - - #endregion - } -} diff --git a/SecureBank.Website/SecureBank.Website.API/APIClient.cs b/SecureBank.Website/SecureBank.Website.API/APIClient.cs index 64ab755..c504147 100644 --- a/SecureBank.Website/SecureBank.Website.API/APIClient.cs +++ b/SecureBank.Website/SecureBank.Website.API/APIClient.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using SecureBank.Common; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -31,28 +32,32 @@ namespace SecureBank.Website.API #region PUBLIC METHODS - public async Task> SendAsync(APIMethodType type, string url) + public async Task> SendAsync(APIMethodType type, string url, Dictionary? query = null) { - return await SendRequestAsync>(type, url, null); + url = AddQuery(url, query); + return await SendRequestAndParseBodyAsync(type, url, null); } - public async Task SendAsync(APIMethodType type, string url) + public async Task SendAsync(APIMethodType type, string url, Dictionary? query = null) { - return await SendRequestAsync(type, url, null); + url = AddQuery(url, query); + return await SendRequestAndParseBodyAsync(type, url, null); } - public async Task> SendAsync(APIMethodType type, string url, TBody body) + public async Task> SendAsync(APIMethodType type, string url, TBody body, Dictionary? query = null) { + url = AddQuery(url, query); HttpContent content = PrepareBody(body); - return await SendRequestAsync>(type, url, content); + return await SendRequestAndParseBodyAsync(type, url, content); } - public async Task SendAsync(APIMethodType type, string url, TBody body) + public async Task SendAsync(APIMethodType type, string url, TBody body, Dictionary? query = null) { + url = AddQuery(url, query); HttpContent content = PrepareBody(body); - return await SendRequestAsync(type, url, content); + return await SendRequestAndParseBodyAsync(type, url, content); } #endregion @@ -61,6 +66,25 @@ namespace SecureBank.Website.API #region PRIVATE METHODS + private string AddQuery(string url, Dictionary? query) + { + if (query is not null && query.Count > 0) + { + Dictionary queryNew = query.ToDictionary(); + StringBuilder sb = new StringBuilder(url); + KeyValuePair item = queryNew.ElementAt(0); + queryNew.Remove(item.Key); + sb.Append($"?{item.Key}={item.Value}"); + + foreach (KeyValuePair item2 in queryNew) + { + sb.Append($"&{item2.Key}={item2.Value}"); + } + return sb.ToString(); + } + return url; + } + private HttpContent PrepareBody(T body) { string json = JsonConvert.SerializeObject(body); @@ -71,37 +95,103 @@ namespace SecureBank.Website.API return content; } - private async Task SendRequestAsync(APIMethodType type, string url, HttpContent? content) + private async Task SendRequestAndParseBodyAsync(APIMethodType type, string url, HttpContent? content) { try { - HttpResponseMessage response = type switch - { - APIMethodType.GET => await _httpClient.GetAsync(url), - APIMethodType.POST => await _httpClient.PostAsync(url, content), - APIMethodType.PUT => await _httpClient.PutAsync(url, content), - APIMethodType.DELETE => await _httpClient.DeleteAsync(url), - _ => throw new NotImplementedException() - }; + HttpResponseMessage response = await SendRequestAsync(type, url, content); + + string stringResponse = await response.Content.ReadAsStringAsync(); - string responseBodyString = await response.Content.ReadAsStringAsync(); - - T? responseBodyObject = JsonConvert.DeserializeObject(responseBodyString); + APIResponse? responseBodyObject = JsonConvert.DeserializeObject(stringResponse); if (responseBodyObject is null) { - throw new Exception($"Wrong response type. Response: {responseBodyString}; {response.StatusCode}"); + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return new APIResponse + { + Status = ResponseStatus.Unauthorized, + Message = $"You do not have permission" + }; + } + else + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"Wrong response type. Response: {stringResponse}; {response.StatusCode}" + }; + } } return responseBodyObject; } catch (Exception ex) { - Console.WriteLine(ex); - throw; + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = ex.Message + }; } } + private async Task> SendRequestAndParseBodyAsync(APIMethodType type, string url, HttpContent? content) + { + try + { + HttpResponseMessage response = await SendRequestAsync(type, url, content); + + string stringResponse = await response.Content.ReadAsStringAsync(); + + APIResponse? responseBodyObject = JsonConvert.DeserializeObject>(stringResponse); + + if (responseBodyObject is null) + { + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return new APIResponse + { + Status = ResponseStatus.Unauthorized, + Message = $"You do not have permission" + }; + } + else + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = $"Wrong response type. Response: {stringResponse}; {response.StatusCode}" + }; + } + } + + return responseBodyObject; + } + catch (Exception ex) + { + return new APIResponse + { + Status = ResponseStatus.BadRequest, + Message = ex.Message + }; + } + } + + private async Task SendRequestAsync(APIMethodType type, string url, HttpContent? content) + { + return type switch + { + APIMethodType.GET => await _httpClient.GetAsync(url), + APIMethodType.POST => await _httpClient.PostAsync(url, content), + APIMethodType.PUT => await _httpClient.PutAsync(url, content), + APIMethodType.PATCH => await _httpClient.PatchAsync(url, content), + APIMethodType.DELETE => await _httpClient.DeleteAsync(url), + _ => throw new NotImplementedException() + }; + } + #endregion } } diff --git a/SecureBank.Website/SecureBank.Website.API/APIEndpointsConfiguration.cs b/SecureBank.Website/SecureBank.Website.API/APIEndpointsConfiguration.cs index 9195973..9a37aa5 100644 --- a/SecureBank.Website/SecureBank.Website.API/APIEndpointsConfiguration.cs +++ b/SecureBank.Website/SecureBank.Website.API/APIEndpointsConfiguration.cs @@ -16,9 +16,25 @@ namespace SecureBank.Website.API // Accounts public string AccountsBase { get; private set; } public string AccountsCreateAccount { get; private set; } + public string AccountsChangePassword { get; private set; } public string AccountsGetPasswordVariant { get; private set; } public string AccountsAuthentication { get; private set; } public string AccountsAuthenticationRefresh { get; private set; } + public string AccountsGetAccounts { get; private set; } + public string AccountsResetPassword { get; private set; } + public string AccountsUnlockAccount { get; private set; } + + // Balance + public string BalanceBase { get; private set; } + public string BalanceGetAccountBalance { get; private set; } + public string BalanceGetBalance { get; private set; } + + // Transfers + public string TransfersBase { get; private set; } + public string TransfersGetUserTransfers { get; private set; } + public string TransfersGetTransfers { get; private set; } + public string TransfersCreateAdminTransfer { get; private set; } + public string TransfersCreateUserTransfer { get; private set; } #endregion @@ -32,9 +48,23 @@ namespace SecureBank.Website.API AccountsBase = $"{Base}{configuration.GetSection("Endpoints").GetSection("Accounts")["Base"]}"; AccountsCreateAccount = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["CreateAccount"]}"; + AccountsChangePassword = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["ChangePassword"]}"; AccountsGetPasswordVariant = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["GetPasswordVariant"]}"; AccountsAuthentication = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["Authentication"]}"; AccountsAuthenticationRefresh = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["AuthenticationRefresh"]}"; + AccountsGetAccounts = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["GetAccounts"]}"; + AccountsResetPassword = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["ResetPassword"]}"; + AccountsUnlockAccount = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["UnlockAccount"]}"; + + BalanceBase = $"{Base}{configuration.GetSection("Endpoints").GetSection("Balance")["Base"]}"; + BalanceGetAccountBalance = $"{BalanceBase}{configuration.GetSection("Endpoints").GetSection("Balance")["GetAccountBalance"]}"; + BalanceGetBalance = $"{BalanceBase}{configuration.GetSection("Endpoints").GetSection("Balance")["GetBalance"]}"; + + TransfersBase = $"{Base}{configuration.GetSection("Endpoints").GetSection("Transfers")["Base"]}"; + TransfersGetTransfers = $"{TransfersBase}{configuration.GetSection("Endpoints").GetSection("Transfers")["GetTransfers"]}"; + TransfersGetUserTransfers = $"{TransfersBase}{configuration.GetSection("Endpoints").GetSection("Transfers")["GetUserTransfers"]}"; + TransfersCreateAdminTransfer = $"{TransfersBase}{configuration.GetSection("Endpoints").GetSection("Transfers")["CreateAdminTransfer"]}"; + TransfersCreateUserTransfer = $"{TransfersBase}{configuration.GetSection("Endpoints").GetSection("Transfers")["CreateUserTransfer"]}"; } #endregion diff --git a/SecureBank.Website/SecureBank.Website.API/APIMethodType.cs b/SecureBank.Website/SecureBank.Website.API/APIMethodType.cs index 586483f..4265bf0 100644 --- a/SecureBank.Website/SecureBank.Website.API/APIMethodType.cs +++ b/SecureBank.Website/SecureBank.Website.API/APIMethodType.cs @@ -11,6 +11,7 @@ namespace SecureBank.Website.API GET, POST, PUT, + PATCH, DELETE } } diff --git a/SecureBank.Website/SecureBank.Website.Authentication/SecureBank.Website.Authentication.csproj b/SecureBank.Website/SecureBank.Website.Authentication/SecureBank.Website.Authentication.csproj index 60ac389..803d75b 100644 --- a/SecureBank.Website/SecureBank.Website.Authentication/SecureBank.Website.Authentication.csproj +++ b/SecureBank.Website/SecureBank.Website.Authentication/SecureBank.Website.Authentication.csproj @@ -9,10 +9,10 @@ + - diff --git a/SecureBank.Website/SecureBank.Website.Authentication/TokenAuthenticationStateProvider.cs b/SecureBank.Website/SecureBank.Website.Authentication/TokenAuthenticationStateProvider.cs index f573037..e2d86f2 100644 --- a/SecureBank.Website/SecureBank.Website.Authentication/TokenAuthenticationStateProvider.cs +++ b/SecureBank.Website/SecureBank.Website.Authentication/TokenAuthenticationStateProvider.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http.Headers; using System.Security.Claims; +using System.IdentityModel.Tokens.Jwt; using System.Text; using System.Threading.Tasks; @@ -27,8 +28,8 @@ namespace SecureBank.Website.Authentication #region CONSTRUCTORS - public TokenAuthenticationStateProvider(IAccountsService accountsService, AuthenticationHelper authenticationHelper, HttpClient httpClient) - { + public TokenAuthenticationStateProvider(IAccountsService accountsService, AuthenticationHelper authenticationHelper, HttpClient httpClient) + { _accountsService = accountsService; _authenticationHelper = authenticationHelper; _httpClient = httpClient; @@ -56,16 +57,22 @@ namespace SecureBank.Website.Authentication APIResponse refreshResponse = await _accountsService.AuthenticationRefresh(); - if (!refreshResponse.Success) + if (refreshResponse.Status != ResponseStatus.Ok) { + await _authenticationHelper.RemoveToken(); _httpClient.DefaultRequestHeaders.Authorization = null; return state; } token = refreshResponse.Data; + await _authenticationHelper.SaveToken(token); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - state = new AuthenticationState(new ClaimsPrincipal()); //TODO: Add claims + + JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); + JwtSecurityToken tokenParsed = tokenHandler.ReadJwtToken(token); + state = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(tokenParsed.Claims))); return state; } diff --git a/SecureBank.Website/SecureBank.Website.Services/AccountsService.cs b/SecureBank.Website/SecureBank.Website.Services/AccountsService.cs index 745ad5f..1c18734 100644 --- a/SecureBank.Website/SecureBank.Website.Services/AccountsService.cs +++ b/SecureBank.Website/SecureBank.Website.Services/AccountsService.cs @@ -15,6 +15,10 @@ namespace SecureBank.Website.Services Task> GetPasswordVariant(int accountId); Task> Authentication(int accountId, AuthenticationRequest data); Task> AuthenticationRefresh(); + Task ChangePassword(ChangePasswordRequest data); + Task>> GetAccounts(int? id = null, string? iban = null); + Task ResetPassword(int accountId); + Task UnlockAccount(int accountId); } @@ -57,8 +61,7 @@ namespace SecureBank.Website.Services public async Task> Authentication(int accountId, AuthenticationRequest data) { - string url = string.Format(_configuration.AccountsAuthentication, accountId); - return await _apiClient.SendAsync(APIMethodType.POST, url, data); + return await _apiClient.SendAsync(APIMethodType.POST, _configuration.AccountsAuthentication, data); } public async Task> AuthenticationRefresh() @@ -66,6 +69,37 @@ namespace SecureBank.Website.Services return await _apiClient.SendAsync(APIMethodType.POST, _configuration.AccountsAuthenticationRefresh); } + public async Task ChangePassword(ChangePasswordRequest data) + { + return await _apiClient.SendAsync(APIMethodType.PATCH, _configuration.AccountsChangePassword, data); + } + + public async Task>> GetAccounts(int? id = null, string? iban = null) + { + Dictionary query = new Dictionary(); + if (id.HasValue) + { + query.Add("id", id.Value.ToString()); + } + if (iban is not null) + { + query.Add("iban", iban); + } + return await _apiClient.SendAsync>(APIMethodType.GET, _configuration.AccountsGetAccounts, query); + } + + public async Task ResetPassword(int accountId) + { + string url = string.Format(_configuration.AccountsResetPassword, accountId); + return await _apiClient.SendAsync(APIMethodType.PATCH, url); + } + + public async Task UnlockAccount(int accountId) + { + string url = string.Format(_configuration.AccountsUnlockAccount, accountId); + return await _apiClient.SendAsync(APIMethodType.PATCH, url); + } + #endregion } } diff --git a/SecureBank.Website/SecureBank.Website.Services/BalanceService.cs b/SecureBank.Website/SecureBank.Website.Services/BalanceService.cs new file mode 100644 index 0000000..5a18ffa --- /dev/null +++ b/SecureBank.Website/SecureBank.Website.Services/BalanceService.cs @@ -0,0 +1,58 @@ +using SecureBank.Common.Accounts; +using SecureBank.Common; +using SecureBank.Website.API; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecureBank.Website.Services +{ + public interface IBalanceService + { + Task> GetAccountBalance(int accountId); + Task> GetBalance(); + } + + + + public class BalanceService : IBalanceService + { + #region FIELDS + + private readonly APIClient _apiClient; + private readonly APIEndpointsConfiguration _configuration; + + #endregion + + + + #region CONSTRUCTORS + + public BalanceService(APIClient apiClient, APIEndpointsConfiguration configuration) + { + _apiClient = apiClient; + _configuration = configuration; + } + + #endregion + + + + #region METHODS + + public async Task> GetAccountBalance(int accountId) + { + string url = string.Format(_configuration.BalanceGetAccountBalance, accountId); + return await _apiClient.SendAsync(APIMethodType.GET, url); + } + + public async Task> GetBalance() + { + return await _apiClient.SendAsync(APIMethodType.GET, _configuration.BalanceGetBalance); + } + + #endregion + } +} diff --git a/SecureBank.Website/SecureBank.Website.Services/TransfersService.cs b/SecureBank.Website/SecureBank.Website.Services/TransfersService.cs new file mode 100644 index 0000000..af1c05a --- /dev/null +++ b/SecureBank.Website/SecureBank.Website.Services/TransfersService.cs @@ -0,0 +1,72 @@ +using SecureBank.Common.Accounts; +using SecureBank.Common; +using SecureBank.Website.API; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SecureBank.Common.Transfers; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace SecureBank.Website.Services +{ + public interface ITransfersService + { + Task CreateAdminTransfer(CreateAdminTransferRequest data); + Task CreateUserTransfer(CreateUserTransferRequest data); + Task>> GetTransfers(); + Task>> GetUserTransfers(int accountId); + } + + + + public class TransfersService : ITransfersService + { + #region FIELDS + + private readonly APIClient _apiClient; + private readonly APIEndpointsConfiguration _configuration; + + #endregion + + + + #region CONSTRUCTORS + + public TransfersService(APIClient apiClient, APIEndpointsConfiguration configuration) + { + _apiClient = apiClient; + _configuration = configuration; + } + + #endregion + + + + #region METHODS + + public async Task>> GetTransfers() + { + return await _apiClient.SendAsync>(APIMethodType.GET, _configuration.TransfersGetTransfers); + } + + public async Task>> GetUserTransfers(int accountId) + { + string url = string.Format(_configuration.TransfersGetUserTransfers, accountId); + return await _apiClient.SendAsync>(APIMethodType.GET, url); + } + + public async Task CreateAdminTransfer(CreateAdminTransferRequest data) + { + return await _apiClient.SendAsync(APIMethodType.POST, _configuration.TransfersCreateAdminTransfer, data); + } + + public async Task CreateUserTransfer(CreateUserTransferRequest data) + { + return await _apiClient.SendAsync(APIMethodType.POST, _configuration.TransfersCreateUserTransfer, data); + } + + #endregion + } +} diff --git a/SecureBank.sln b/SecureBank.sln index 76d0c6e..edb04fc 100644 --- a/SecureBank.sln +++ b/SecureBank.sln @@ -29,15 +29,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.API.Authenticati EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Website.Authentication", "SecureBank.Website\SecureBank.Website.Authentication\SecureBank.Website.Authentication.csproj", "{4BC964A3-91C9-47FD-9A78-1E43301E9779}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Helpers", "SecureBank.Helpers\SecureBank.Helpers.csproj", "{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A111947-FD57-4EF7-8B03-C46E73B68DE1}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore database.db = database.db - Dockerfile = Dockerfile + docker-compose.yml = docker-compose.yml + Main.Dockerfile = Main.Dockerfile + Nginx.Dockerfile = Nginx.Dockerfile EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Nginx", "Nginx", "{E82E5EB5-85CD-4E15-A5F7-AC1D0F99841E}" + ProjectSection(SolutionItems) = preProject + localhost.conf = localhost.conf + Nginx\nginx.conf = Nginx\nginx.conf + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Authentication", "SecureBank.Authentication\SecureBank.Authentication.csproj", "{3A36A494-9B6C-4E18-AE67-064ACD57BAB6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.API.Encryption", "SecureBank.API\SecureBank.API.Encryption\SecureBank.API.Encryption.csproj", "{D5988236-1C22-4A5B-B1E0-A4258A9B1A1F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,10 +98,14 @@ Global {4BC964A3-91C9-47FD-9A78-1E43301E9779}.Debug|Any CPU.Build.0 = Debug|Any CPU {4BC964A3-91C9-47FD-9A78-1E43301E9779}.Release|Any CPU.ActiveCfg = Release|Any CPU {4BC964A3-91C9-47FD-9A78-1E43301E9779}.Release|Any CPU.Build.0 = Release|Any CPU - {F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Release|Any CPU.Build.0 = Release|Any CPU + {3A36A494-9B6C-4E18-AE67-064ACD57BAB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A36A494-9B6C-4E18-AE67-064ACD57BAB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A36A494-9B6C-4E18-AE67-064ACD57BAB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A36A494-9B6C-4E18-AE67-064ACD57BAB6}.Release|Any CPU.Build.0 = Release|Any CPU + {D5988236-1C22-4A5B-B1E0-A4258A9B1A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5988236-1C22-4A5B-B1E0-A4258A9B1A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5988236-1C22-4A5B-B1E0-A4258A9B1A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5988236-1C22-4A5B-B1E0-A4258A9B1A1F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +118,8 @@ Global {41FA4292-92B0-4810-8127-6E70E2073D38} = {E23C57D5-1527-4482-963B-374CF1A098D5} {54220F73-24DE-47BB-A027-5326E90A59B6} = {E23C57D5-1527-4482-963B-374CF1A098D5} {4BC964A3-91C9-47FD-9A78-1E43301E9779} = {EFBD13EE-AF89-4792-A3DE-FF38BDA38DD0} + {E82E5EB5-85CD-4E15-A5F7-AC1D0F99841E} = {6A111947-FD57-4EF7-8B03-C46E73B68DE1} + {D5988236-1C22-4A5B-B1E0-A4258A9B1A1F} = {E23C57D5-1527-4482-963B-374CF1A098D5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {626ACE64-EFF9-4C9A-84DC-DFF35D76AF59} diff --git a/SecureBank/Components/Layout/MainLayout.razor b/SecureBank/Components/Layout/MainLayout.razor index c596361..8e98f53 100644 --- a/SecureBank/Components/Layout/MainLayout.razor +++ b/SecureBank/Components/Layout/MainLayout.razor @@ -2,12 +2,20 @@
- Login + @if (_claims is null) + { + + } + else + { +

Logged as: @($"{_claims.FirstName} {_claims.LastName} ({_claims.AccountId:00000000})")

+ + }
+ +@code +{ + #region SERVICES + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + [Inject] + protected AuthenticationHelper _authenticationHelper { get; set; } + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + #endregion + + + + #region METHODS + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); + _claims = null; + if (authState.User.Claims.Any()) + { + _claims = new Claims(authState.User.Claims); + } + StateHasChanged(); + } + } + + protected void Login() + { + _navigationManager.NavigateTo("/auth"); + } + + protected async Task Logout() + { + await _authenticationHelper.RemoveToken(); + _navigationManager.NavigateTo("/", true); + } + + #endregion +} \ No newline at end of file diff --git a/SecureBank/Components/Layout/NavMenu.razor b/SecureBank/Components/Layout/NavMenu.razor index 5e27fad..fd320ae 100644 --- a/SecureBank/Components/Layout/NavMenu.razor +++ b/SecureBank/Components/Layout/NavMenu.razor @@ -12,7 +12,34 @@ Home + @if (Claims is not null) + { + + Dashboard + + + Create transfer + + + Account details + + @if (Claims.IsAdmin) + { + + Admin panel + + } + } +@code +{ + #region PARAMETERS + + [Parameter] + public Claims? Claims { get; set; } + + #endregion +} \ No newline at end of file diff --git a/SecureBank/Components/Layout/NavMenu.razor.css b/SecureBank/Components/Layout/NavMenu.razor.css index 4e15395..2b4aa84 100644 --- a/SecureBank/Components/Layout/NavMenu.razor.css +++ b/SecureBank/Components/Layout/NavMenu.razor.css @@ -46,6 +46,22 @@ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); } +.bi-admin-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-gear-wide-connected' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M7.068.727c.243-.97 1.62-.97 1.864 0l.071.286a.96.96 0 0 0 1.622.434l.205-.211c.695-.719 1.888-.03 1.613.931l-.08.284a.96.96 0 0 0 1.187 1.187l.283-.081c.96-.275 1.65.918.931 1.613l-.211.205a.96.96 0 0 0 .434 1.622l.286.071c.97.243.97 1.62 0 1.864l-.286.071a.96.96 0 0 0-.434 1.622l.211.205c.719.695.03 1.888-.931 1.613l-.284-.08a.96.96 0 0 0-1.187 1.187l.081.283c.275.96-.918 1.65-1.613.931l-.205-.211a.96.96 0 0 0-1.622.434l-.071.286c-.243.97-1.62.97-1.864 0l-.071-.286a.96.96 0 0 0-1.622-.434l-.205.211c-.695.719-1.888.03-1.613-.931l.08-.284a.96.96 0 0 0-1.186-1.187l-.284.081c-.96.275-1.65-.918-.931-1.613l.211-.205a.96.96 0 0 0-.434-1.622l-.286-.071c-.97-.243-.97-1.62 0-1.864l.286-.071a.96.96 0 0 0 .434-1.622l-.211-.205c-.719-.695-.03-1.888.931-1.613l.284.08a.96.96 0 0 0 1.187-1.186l-.081-.284c-.275-.96.918-1.65 1.613-.931l.205.211a.96.96 0 0 0 1.622-.434zM12.973 8.5H8.25l-2.834 3.779A4.998 4.998 0 0 0 12.973 8.5m0-1a4.998 4.998 0 0 0-7.557-3.779l2.834 3.78zM5.048 3.967l-.087.065zm-.431.355A4.98 4.98 0 0 0 3.002 8c0 1.455.622 2.765 1.615 3.678L7.375 8zm.344 7.646.087.065z'/%3E%3C/svg%3E"); +} + +.bi-transfer-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-left-right' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1 11.5a.5.5 0 0 0 .5.5h11.793l-3.147 3.146a.5.5 0 0 0 .708.708l4-4a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 11H1.5a.5.5 0 0 0-.5.5m14-7a.5.5 0 0 1-.5.5H2.707l3.147 3.146a.5.5 0 1 1-.708.708l-4-4a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H14.5a.5.5 0 0 1 .5.5'/%3E%3C/svg%3E"); +} + +.bi-dashboard-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-bank2' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.277.084a.5.5 0 0 0-.554 0l-7.5 5A.5.5 0 0 0 .5 6h1.875v7H1.5a.5.5 0 0 0 0 1h13a.5.5 0 1 0 0-1h-.875V6H15.5a.5.5 0 0 0 .277-.916zM12.375 6v7h-1.25V6zm-2.5 0v7h-1.25V6zm-2.5 0v7h-1.25V6zm-2.5 0v7h-1.25V6zM8 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2M.5 15a.5.5 0 0 0 0 1h15a.5.5 0 1 0 0-1z'/%3E%3C/svg%3E"); +} + +.bi-account-details-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z'/%3E%3C/svg%3E"); +} + .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; diff --git a/SecureBank/Components/Pages/Admin/Accounts.razor b/SecureBank/Components/Pages/Admin/Accounts.razor new file mode 100644 index 0000000..18467e2 --- /dev/null +++ b/SecureBank/Components/Pages/Admin/Accounts.razor @@ -0,0 +1,185 @@ +@page "/admin/accounts-management" + +

Accounts

+ +@if (_authLoaded) +{ + @if (_claims is not null && _claims.IsAdmin) + { + @switch (_dataLoadedState) + { + case DataLoadState.Loading: +

Waiting for data...

+ break; + case DataLoadState.NotLoaded: +

Data cannot be loaded. Try again later.

+ Click here to redirect to main page + break; + case DataLoadState.Loaded: + @if (_errorMessage is not null) + { +

@_errorMessage

+ } + + + + + + + + + + + + @foreach (var account in _accounts) + { + + + + + + + + + + + } +
IdNameEmailPhone numberIBANIs password temporaryIs lockedOperations
@account.Id@account.FirstName @account.LastName@account.Email@account.PhoneNumber@account.IBAN@(account.TemporaryPassword ? "YES" : "NO")@(account.LockReason is not null || account.LoginFailedCount >= 3 ? "YES" : "NO") + + +
+ break; + } + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } +} +else +{ +

Waiting for authorization...

+} + +@code { + #region ENUMS + + protected enum DataLoadState + { + Loading, + Loaded, + NotLoaded + } + + #endregion + + + + #region SERVICES + + [Inject] + protected IAccountsService _accountsService { get; set; } + + [Inject] + protected IBalanceService _balanceService { get; set; } + + [Inject] + protected ITransfersService _transfersService { get; set; } + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + protected bool _authLoaded; + + protected DataLoadState _dataLoadedState; + + protected IEnumerable _accounts; + + protected string? _errorMessage; + + #endregion + + + + #region METHODS + + protected override void OnInitialized() + { + _claims = null; + _authLoaded = false; + _dataLoadedState = DataLoadState.Loading; + _errorMessage = null; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + + StateHasChanged(); + + if (_authLoaded) + { + APIResponse> accountResponse = await _accountsService.GetAccounts(); + if (accountResponse.Status == ResponseStatus.Ok) + { + _accounts = accountResponse.Data; + } + else if (accountResponse.Status == ResponseStatus.Unauthorized) + { + _claims = null; + } + + _dataLoadedState = accountResponse.Status == ResponseStatus.Ok ? DataLoadState.Loaded : DataLoadState.NotLoaded; + + StateHasChanged(); + } + } + } + + protected async Task ResetPassword(int id) + { + APIResponse response = await _accountsService.ResetPassword(id); + if (response.Status == ResponseStatus.Ok) + { + _navigationManager.Refresh(true); + } + else + { + _errorMessage = $"An error occured while reseting password for account with id {id}. {response.Message}"; + } + } + + protected async Task Unlock(int id) + { + APIResponse response = await _accountsService.UnlockAccount(id); + if (response.Status == ResponseStatus.Ok) + { + _navigationManager.Refresh(true); + } + else + { + _errorMessage = $"An error occured while unlocking account with id {id}. {response.Message}"; + } + } + + #endregion +} diff --git a/SecureBank/Components/Pages/Admin/CreateTransfer.razor b/SecureBank/Components/Pages/Admin/CreateTransfer.razor new file mode 100644 index 0000000..07d4185 --- /dev/null +++ b/SecureBank/Components/Pages/Admin/CreateTransfer.razor @@ -0,0 +1,337 @@ +@page "/admin/create-transfer" + +

Create new transfer

+ +@if (_authLoaded) +{ + @if (_claims is not null && _claims.IsAdmin) + { + @switch (_stage) + { + case (Stage.Form): + +
+

Sender data:

+
+ + +
+
+ + +
+
+ + +
+
+
+

Receiver data:

+
+ + +
+
+ + +
+
+ + +
+
+
+

Transfer data:

+
+ + +
+
+ + +
+
+
+ +
+ break; + case (Stage.Validated): +

Sender data:

+ + + + + + + + + + + + + + + + + +
Account number:@_data.SenderAccountNumber
Name:@(_data.SenderName ?? "")
Address:@(_data.SenderAddress ?? "")
Account balance after transfer:@_senderAmount
+
+
+

Receiver data:

+ + + + + + + + + + + + + + + + + +
Account number:@_data.ReceiverAccountNumber
Name:@(_data.ReceiverName ?? "")
Address:@(_data.ReceiverAddress ?? "")
Account balance after transfer:@_receiverAmount
+
+
+

Transfer data:

+ + + + + + + + + +
Title:@(_data.Title ?? "")
Amount:@_data.Amount PLN
+ +
+ + + break; + case (Stage.Accepted): +

Transfer for amount @_data.Amount PLN was successfully sent from account with number @_data.SenderAccountNumber to account with number @_data.ReceiverAccountNumber

+ + + break; + } + + @if (!string.IsNullOrWhiteSpace(_errorMessage)) + { +

Error: @_errorMessage

+ } + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } +} +else +{ +

Waiting for authorization...

+} + + +@code +{ + #region ENUMS + + protected enum Stage + { + Form, + Validated, + Accepted + } + + #endregion + + + #region SERVICES + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + [Inject] + protected IAccountsService _accountsService { get; set; } + + [Inject] + protected IBalanceService _balanceService { get; set; } + + [Inject] + protected ITransfersService _transfersService { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + protected bool _authLoaded; + + protected Stage _stage; + + protected string? _errorMessage; + + protected CreateAdminTransferRequest _data; + + protected string _senderAmount; + protected string _receiverAmount; + + #endregion + + + + #region METHODS + + protected override void OnInitialized() + { + _claims = null; + _authLoaded = false; + _stage = Stage.Form; + _errorMessage = null; + _data = new CreateAdminTransferRequest(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + StateHasChanged(); + } + } + + protected async void SubmitToValidation() + { + if (_data.SenderAccountNumber is null) + { + _errorMessage = "Sender account number cannot be empty"; + return; + } + + if (!_data.SenderAccountNumber.All(x => char.IsDigit(x))) + { + _errorMessage = "Wrong sender account number format. Account number consists only of digits"; + return; + } + + if (_data.SenderAccountNumber.Length != 26) + { + _errorMessage = "Wrong sender account number format. Account number must have length of 26 digits"; + return; + } + + if (_data.ReceiverAccountNumber is null) + { + _errorMessage = "Receiver account number cannot be empty"; + return; + } + + if (!_data.ReceiverAccountNumber.All(x => char.IsDigit(x))) + { + _errorMessage = "Wrong receiver account number format. Account number consists only of digits"; + return; + } + + if (_data.ReceiverAccountNumber.Length != 26) + { + _errorMessage = "Wrong receiver account number format. Account number must have length of 26 digits"; + return; + } + + if (_data.Amount <= 0) + { + _errorMessage = "Transfer amount has to be greater than 0 PLN"; + return; + } + + _data.Amount = Math.Round(_data.Amount, 2, MidpointRounding.ToEven); + + _senderAmount = await GetBalance(_data.SenderAccountNumber, -_data.Amount); + _receiverAmount = await GetBalance(_data.ReceiverAccountNumber, _data.Amount); + + _stage = Stage.Validated; + _errorMessage = null; + StateHasChanged(); + } + + protected async Task GetBalance(string accountNumber, decimal amount) + { + APIResponse> senderAccountResponse = await _accountsService.GetAccounts(iban: accountNumber); + if (senderAccountResponse.Status == ResponseStatus.Ok && senderAccountResponse.Data.Count() == 1) + { + APIResponse senderAmountResponse = await _balanceService.GetAccountBalance(senderAccountResponse.Data.ElementAt(0).Id); + if (senderAmountResponse.Status == ResponseStatus.Ok) + { + return $"{(senderAmountResponse.Data + amount):F2} PLN"; + } + else + { + return ""; + } + } + else + { + return ""; + } + } + + protected async Task SubmitTransfer() + { + APIResponse response = await _transfersService.CreateAdminTransfer(_data); + if (response.Status == ResponseStatus.Ok) + { + _errorMessage = null; + _stage = Stage.Accepted; + StateHasChanged(); + } + else + { + _errorMessage = response.Message; + } + } + + protected void NavigateToAdminPanel() + { + _navigationManager.NavigateTo("/admin", true); + } + + protected void NavigateToNewForm() + { + _navigationManager.Refresh(true); + } + + #endregion +} diff --git a/SecureBank/Components/Pages/Admin/Panel.razor b/SecureBank/Components/Pages/Admin/Panel.razor new file mode 100644 index 0000000..01ba9e4 --- /dev/null +++ b/SecureBank/Components/Pages/Admin/Panel.razor @@ -0,0 +1,70 @@ +@page "/admin" + +

Admin panel

+ +@if (_authLoaded) +{ + @if (_claims is not null && _claims.IsAdmin) + { + + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } +} +else +{ +

Waiting for authorization...

+} + + +@code { + #region SERVICES + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + protected bool _authLoaded; + + #endregion + + + + #region METHODS + + protected override void OnInitialized() + { + _claims = null; + _authLoaded = false; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + StateHasChanged(); + } + } + + #endregion +} diff --git a/SecureBank/Components/Pages/Admin/Register.razor b/SecureBank/Components/Pages/Admin/Register.razor index 39cac9c..c15f6fe 100644 --- a/SecureBank/Components/Pages/Admin/Register.razor +++ b/SecureBank/Components/Pages/Admin/Register.razor @@ -2,76 +2,122 @@

Create new client account

-@if (_id is null) +@if (_authLoaded || true) { - -
- - -
-
- - -
-
- - -
-
- - -
-
- - @if (!string.IsNullOrWhiteSpace(_message)) + @if (true || (_claims is not null && _claims.IsAdmin)) + { + @if (_id is null) { -

Error: @_message

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + @if (!string.IsNullOrWhiteSpace(_message)) + { +

Error: @_message

+ } +
} -
+ else + { +

New client account was created

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Client code:@($"{_id:00000000}")
First name:@_data.FirstName
Last name:@_data.LastName
Email:@_data.Email
Phone number:@_data.PhoneNumber
Address:@_data.Address
PESEL:@_data.PESEL
Id card number:@_data.IdCardNumber
Password:******** (Information passed on to the client - LOG)
+
+ + + + } + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } } else { -

New client account was created

- - - - - - - - - - - - - - - - - - - - - - - - - -
Client code:@($"{_id:00000000}")
First name:@_data.FirstName
Last name:@_data.LastName
Email:@_data.Email
Phone number:@_data.PhoneNumber
Password:******** (Information passed on to the client - LOG)
-
- - - +

Waiting for authorization...

} + @code { #region SERVICES @@ -81,6 +127,9 @@ else [Inject] protected NavigationManager _navigationManager { get; set; } + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + #endregion @@ -93,6 +142,10 @@ else protected string? _message; + protected Claims? _claims; + + protected bool _authLoaded; + #endregion @@ -104,12 +157,28 @@ else _data = new CreateAccountRequest(); _id = null; _message = null; + _claims = null; + _authLoaded = false; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + StateHasChanged(); + } } protected async Task Submit() { APIResponse response = await _accountsService.CreateAccount(_data); - if (response.Success) + if (response.Status == ResponseStatus.Ok) { _id = response.Data; } @@ -121,12 +190,12 @@ else protected void NavigateToNewForm() { - OnInitialized(); + _navigationManager.Refresh(true); } protected void NavigateToAdminPanel() { - _navigationManager.NavigateTo("/"); //TODO: Zmienić na /admin + _navigationManager.NavigateTo("/admin", true); } #endregion diff --git a/SecureBank/Components/Pages/Auth.razor b/SecureBank/Components/Pages/Auth.razor index 109f60f..11cbef8 100644 --- a/SecureBank/Components/Pages/Auth.razor +++ b/SecureBank/Components/Pages/Auth.razor @@ -2,94 +2,142 @@

Login

-@if (!_clientCodeAccepted) -{ -

Enter your client code:

-
- - - - - - - - -
-
- -
-
- @if (!string.IsNullOrWhiteSpace(_clientCodeMessage)) - { -

Error: @_clientCodeMessage

- } -} -else +@switch (_state) { -

Enter your password:

+ case AuthState.CodeInput: +

Enter your client code:

-
- - - - - - - - - - - - - - - - - - - - -
-
- - - @if (!string.IsNullOrWhiteSpace(_clientPasswordMessage)) - { -

Error: @_clientPasswordMessage

- } +
+ + + + + + + + +
+
+ +
+
+ break; + case AuthState.PasswordInput: +

Enter your password:

+ +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + break; + case AuthState.PasswordChange: +

Change temporary password:

+
+
+ + +
+
+ + +
+
+
+ + + break; } +@if (!string.IsNullOrWhiteSpace(_messageError)) +{ +

Error: @_messageError

+} + +@if (!string.IsNullOrWhiteSpace(_messageSuccess)) +{ +

@_messageSuccess

+} @code { + #region ENUMS + + protected enum AuthState + { + CodeInput, + PasswordInput, + PasswordChange + } + + #endregion + + + #region SERVICES [Inject] protected IAccountsService _accountService { get; set; } + [Inject] + protected AuthenticationHelper _authenticationHelper { get; set; } + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + [Inject] + protected HttpClient _httpClient { get; set; } + #endregion #region FIELDS - protected bool _clientCodeAccepted; + protected AuthState _state; - protected string? _clientCodeMessage; + protected string? _messageError; + protected string? _messageSuccess; protected string[] _clientCodeArr; protected string _clientCode => string.Join(string.Empty, _clientCodeArr); protected string[] _clientPasswordArr; protected string _clientPassword => string.Join(string.Empty, _clientPasswordArr); - protected byte _clientPasswordFailCount; - - protected string? _clientPasswordMessage; protected GetPasswordVariantResponse? _loginRequest; + protected int _accountId; + + protected string _password1; + protected string _password2; + #endregion @@ -98,53 +146,116 @@ else protected override void OnInitialized() { - _clientCodeAccepted = false; - _clientCodeMessage = null; + _state = AuthState.CodeInput; + _messageError = null; + _messageSuccess = null; _clientCodeArr = new string[8]; _clientPasswordArr = new string[20]; - _clientPasswordMessage = null; _loginRequest = null; + _accountId = 0; + _password1 = string.Empty; + _password2 = string.Empty; } protected async void SubmitClientCode() { - if (_clientCode.Length == 8 && int.TryParse(_clientCode, out int accountId)) + if (_clientCode.Length == 8 && int.TryParse(_clientCode, out _accountId)) { - APIResponse loginRequest = await _accountService.GetPasswordVariant(accountId); + APIResponse loginRequest = await _accountService.GetPasswordVariant(_accountId); - if (loginRequest.Success) + if (loginRequest.Status == ResponseStatus.Ok) { if (loginRequest.Data.ValidTo < DateTime.Now) { - _clientCodeMessage = "Your login request has already expired. Check your internet connection"; + _messageError = "Your login request has already expired. Check your internet connection"; } else { - _clientCodeAccepted = true; + _state = AuthState.PasswordInput; _loginRequest = loginRequest.Data; + _messageError = null; } } else { - _clientCodeMessage = loginRequest.Message; + _messageError = loginRequest.Message; } } else { - _clientCodeMessage = "Wrong client code format"; + _messageError = "Wrong client code format"; } StateHasChanged(); } - protected void SubmitClientPassword() + protected async Task SubmitClientPassword() { if (_clientPassword.Length == _loginRequest.Indexes.Length) { + AuthenticationRequest requestData = new AuthenticationRequest + { + LoginRequestId = _loginRequest.LoginRequestId, + Password = _clientPassword, + }; + APIResponse response = await _accountService.Authentication(_accountId, requestData); + + if (response.Status == ResponseStatus.Ok) + { + if (response.ActionCode == 2) + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", response.Data); + _state = AuthState.PasswordChange; + } + else + { + await _authenticationHelper.SaveToken(response.Data); + _messageSuccess = "Login succedeed. You will be redirected in 5 seconds"; + StateHasChanged(); + await Task.Delay(5000); + _navigationManager.NavigateTo("/", true); + } + } + else + { + switch (response.ActionCode) + { + case 1: OnInitialized(); StateHasChanged(); break; + } + _messageError = response.Message; + } } else { - _clientPasswordMessage = "Not all fields were filled"; + _messageError = "Not all fields were filled"; + } + StateHasChanged(); + } + + protected async Task SubmitChangePassword() + { + if (string.Equals(_password1, _password2)) + { + ChangePasswordRequest data = new ChangePasswordRequest { Password = _password1 }; + APIResponse response = await _accountService.ChangePassword(data); + if (response.Status == ResponseStatus.Ok) + { + _messageError = null; + _messageSuccess = "Password has been changed. Please, login again. You will be redirected in 5 seconds"; + StateHasChanged(); + Task redirectionTimer = Task.Delay(5000); + _httpClient.DefaultRequestHeaders.Authorization = null; + await redirectionTimer; + OnInitialized(); + } + else + { + _messageError = response.Message; + } + } + else + { + _messageError = "Password fields does not match"; } } diff --git a/SecureBank/Components/Pages/Home.razor b/SecureBank/Components/Pages/Home.razor index 9001e0b..d28bd5f 100644 --- a/SecureBank/Components/Pages/Home.razor +++ b/SecureBank/Components/Pages/Home.razor @@ -2,6 +2,6 @@ Home -

Hello, world!

+

Hello

-Welcome to your new app. +Welcome in SecureBank. diff --git a/SecureBank/Components/Pages/User/AccountDetails.razor b/SecureBank/Components/Pages/User/AccountDetails.razor new file mode 100644 index 0000000..61ff21c --- /dev/null +++ b/SecureBank/Components/Pages/User/AccountDetails.razor @@ -0,0 +1,196 @@ +@page "/account-details" + +

Account details

+ +@if (_authLoaded) +{ + @if (_claims is not null) + { + @switch (_dataLoadedState) + { + case DataLoadState.Loading: +

Waiting for data...

+ break; + case DataLoadState.NotLoaded: +

Data cannot be loaded. Try again later.

+ Click here to redirect to main page + break; + case DataLoadState.Loaded: +

Personal data:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
First name:@_data.FirstName
Last name:@_data.LastName
Email:@_data.Email
Phone number:@_data.PhoneNumber
Address:@_data.Address
IBAN:@_data.IBAN
+
+

Id card data:

+ + + + + + + + + +
PESEL:@(_hideIdCardData ? "***********" : _data.PESEL)
Id card number:@(_hideIdCardData ? "*********" : _data.IdCardNumber)
+ +
+
+

Debit card data:

+ + + + + + + + + + + + + +
Card number:@(_hideDebitCardData ? "****************" : _data.CardNumber)
Expiration date:@(_hideDebitCardData ? "**/**" : _data.CardExpirationDate)
CVV:@(_hideDebitCardData ? "***" : _data.CardCVV)
+ +
+
+

Options:

+ + break; + } + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } +} +else +{ +

Waiting for authorization...

+} + +@code +{ + #region ENUMS + + protected enum DataLoadState + { + Loading, + Loaded, + NotLoaded + } + + #endregion + + + + #region SERVICES + + [Inject] + protected IAccountsService _accountsService { get; set; } + + [Inject] + protected IBalanceService _balanceService { get; set; } + + [Inject] + protected ITransfersService _transfersService { get; set; } + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + protected bool _authLoaded; + + protected DataLoadState _dataLoadedState; + + protected AccountResponse _data; + + protected bool _hideIdCardData; + protected bool _hideDebitCardData; + + #endregion + + + + #region METHODS + + protected override void OnInitialized() + { + _claims = null; + _authLoaded = false; + _dataLoadedState = DataLoadState.Loading; + _data = null; + _hideIdCardData = true; + _hideDebitCardData = true; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + + StateHasChanged(); + + if (_authLoaded) + { + APIResponse> dataResponse = await _accountsService.GetAccounts(id: _claims.AccountId); + if (dataResponse.Status == ResponseStatus.Ok && dataResponse.Data.Count() == 1) + { + _data = dataResponse.Data.ElementAt(0); + _dataLoadedState = DataLoadState.Loaded; + } + else + { + _dataLoadedState = DataLoadState.NotLoaded; + } + StateHasChanged(); + } + } + } + + protected void ChangePassword() + { + _navigationManager.NavigateTo("/account-details/change-password", true); + } + + #endregion +} \ No newline at end of file diff --git a/SecureBank/Components/Pages/User/ChangePassword.razor b/SecureBank/Components/Pages/User/ChangePassword.razor new file mode 100644 index 0000000..691cf67 --- /dev/null +++ b/SecureBank/Components/Pages/User/ChangePassword.razor @@ -0,0 +1,140 @@ +@page "/account-details/change-password" + +

Change password

+ +@if (_authLoaded) +{ + @if (_claims is not null) + { +
+
+ + +
+
+ + +
+
+
+ + @if (!string.IsNullOrWhiteSpace(_messageError)) + { +

Error: @_messageError

+ } + + @if (!string.IsNullOrWhiteSpace(_messageSuccess)) + { +

@_messageSuccess

+ } + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } +} +else +{ +

Waiting for authorization...

+} + +@code { + #region SERVICES + + [Inject] + protected IAccountsService _accountsService { get; set; } + + [Inject] + protected AuthenticationHelper _authenticationHelper { get; set; } + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + [Inject] + protected HttpClient _httpClient { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + protected bool _authLoaded; + + protected string? _messageError; + protected string? _messageSuccess; + + protected string _password1; + protected string _password2; + + #endregion + + + + #region METHODS + + protected override void OnInitialized() + { + _claims = null; + _authLoaded = false; + _messageError = null; + _messageSuccess = null; + _password1 = string.Empty; + _password2 = string.Empty; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + + StateHasChanged(); + } + } + + protected async Task SubmitChangePassword() + { + if (string.Equals(_password1, _password2)) + { + ChangePasswordRequest data = new ChangePasswordRequest { Password = _password1 }; + APIResponse response = await _accountsService.ChangePassword(data); + if (response.Status == ResponseStatus.Ok) + { + _messageError = null; + _messageSuccess = "Password has been changed. You will be logged out in 5 seconds."; + StateHasChanged(); + Task redirectionTimer = Task.Delay(5000); + await _authenticationHelper.RemoveToken(); + _httpClient.DefaultRequestHeaders.Authorization = null; + await redirectionTimer; + _navigationManager.NavigateTo("/", true); + } + else + { + _messageError = response.Message; + } + } + else + { + _messageError = "Password fields does not match"; + } + } + + #endregion +} diff --git a/SecureBank/Components/Pages/User/CreateTransfer.razor b/SecureBank/Components/Pages/User/CreateTransfer.razor new file mode 100644 index 0000000..65cdc1b --- /dev/null +++ b/SecureBank/Components/Pages/User/CreateTransfer.razor @@ -0,0 +1,255 @@ +@page "/create-transfer" + +

Create new transfer

+ +@if (_authLoaded) +{ + @if (_claims is not null) + { + @switch (_stage) + { + case (Stage.Form): + +
+

Receiver data:

+
+ + +
+
+ + +
+
+ + +
+
+
+

Transfer data:

+
+ + +
+
+ + +
+
+
+ +
+ break; + case (Stage.Validated): +

Receiver data:

+ + + + + + + + + + + + + +
Account number:@_data.ReceiverAccountNumber
Name:@(_data.ReceiverName ?? "")
Address:@(_data.ReceiverAddress ?? "")
+
+
+

Transfer data:

+ + + + + + + + + + + + + +
Title:@(_data.Title ?? "")
Amount:@_data.Amount PLN
Account balance after transfer:@_senderAmount
+ +
+ + + break; + case (Stage.Accepted): +

Transfer for amount @_data.Amount PLN was successfully sent from account to account with number @_data.ReceiverAccountNumber

+ + break; + } + + @if (!string.IsNullOrWhiteSpace(_errorMessage)) + { +

Error: @_errorMessage

+ } + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } +} +else +{ +

Waiting for authorization...

+} + +@code { + #region ENUMS + + protected enum Stage + { + Form, + Validated, + Accepted + } + + #endregion + + + #region SERVICES + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + [Inject] + protected IAccountsService _accountsService { get; set; } + + [Inject] + protected IBalanceService _balanceService { get; set; } + + [Inject] + protected ITransfersService _transfersService { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + protected bool _authLoaded; + + protected Stage _stage; + + protected string? _errorMessage; + + protected CreateUserTransferRequest _data; + + protected string _senderAmount; + protected string _receiverAmount; + + #endregion + + + + #region METHODS + + protected override void OnInitialized() + { + _claims = null; + _authLoaded = false; + _stage = Stage.Form; + _errorMessage = null; + _data = new CreateUserTransferRequest(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + StateHasChanged(); + } + } + + protected async void SubmitToValidation() + { + if (_data.ReceiverAccountNumber is null) + { + _errorMessage = "Receiver account number cannot be empty"; + return; + } + + if (!_data.ReceiverAccountNumber.All(x => char.IsDigit(x))) + { + _errorMessage = "Wrong receiver account number format. Account number consists only of digits"; + return; + } + + if (_data.ReceiverAccountNumber.Length != 26) + { + _errorMessage = "Wrong receiver account number format. Account number must have length of 26 digits"; + return; + } + + if (_data.Amount <= 0) + { + _errorMessage = "Transfer amount has to be greater than 0 PLN"; + return; + } + + _data.Amount = Math.Round(_data.Amount, 2, MidpointRounding.ToEven); + + APIResponse senderAmountResponse = await _balanceService.GetAccountBalance(_claims.AccountId); + if (senderAmountResponse.Status == ResponseStatus.Ok) + { + _senderAmount = $"{(senderAmountResponse.Data - _data.Amount):F2} PLN"; + } + else + { + _senderAmount = ""; + } + + _stage = Stage.Validated; + _errorMessage = null; + StateHasChanged(); + } + + protected async Task SubmitTransfer() + { + APIResponse response = await _transfersService.CreateUserTransfer(_data); + if (response.Status == ResponseStatus.Ok) + { + _errorMessage = null; + _stage = Stage.Accepted; + StateHasChanged(); + } + else + { + _errorMessage = response.Message; + } + } + + protected void NavigateToNewForm() + { + _navigationManager.Refresh(true); + } + + #endregion +} diff --git a/SecureBank/Components/Pages/User/Dashboard.razor b/SecureBank/Components/Pages/User/Dashboard.razor new file mode 100644 index 0000000..b6838d7 --- /dev/null +++ b/SecureBank/Components/Pages/User/Dashboard.razor @@ -0,0 +1,152 @@ +@page "/dashboard" + +

Dashboard

+ +@if (_authLoaded) +{ + @if (_claims is not null) + { + @switch (_dataLoadedState) + { + case DataLoadState.Loading: +

Waiting for data...

+ break; + case DataLoadState.NotLoaded: +

Data cannot be loaded. Try again later.

+ Click here to redirect to main page + break; + case DataLoadState.Loaded: +
Account balance:
+

@($"{_balance:F2} PLN")

+
Operation history:
+ + + + + + + @foreach (TransferResponse operation in _operations.OrderByDescending(x => x.Date)) + { + + + + + + } +
DateTitleAmount
@operation.Date.ToString("dd.MM.yyyy HH:mm")@(string.IsNullOrWhiteSpace(operation.Title) ? "" : operation.Title)@($"{(_account.IBAN == operation.SenderAccountNumber ? "- " : string.Empty)}{operation.Amount:F2} PLN")
+ break; + } + } + else + { +

You do not have permission to view this page

+ Click here to redirect to main page + } +} +else +{ +

Waiting for authorization...

+} + + +@code { + #region ENUMS + + protected enum DataLoadState + { + Loading, + Loaded, + NotLoaded + } + + #endregion + + + + #region SERVICES + + [Inject] + protected IAccountsService _accountsService { get; set; } + + [Inject] + protected IBalanceService _balanceService { get; set; } + + [Inject] + protected ITransfersService _transfersService { get; set; } + + [Inject] + protected NavigationManager _navigationManager { get; set; } + + [Inject] + protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; } + + #endregion + + + + #region FIELDS + + protected Claims? _claims; + + protected bool _authLoaded; + + protected DataLoadState _dataLoadedState; + + protected decimal _balance; + protected IEnumerable _operations; + protected AccountResponse _account; + + #endregion + + + + #region METHODS + + protected override void OnInitialized() + { + _claims = null; + _authLoaded = false; + _dataLoadedState = DataLoadState.Loading; + _balance = 0.00M; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync(); + if (state.User.Claims.Any()) + { + _claims = new Claims(state.User.Claims); + } + _authLoaded = true; + + StateHasChanged(); + + if (_authLoaded) + { + APIResponse balanceResponse = await _balanceService.GetBalance(); + if (balanceResponse.Status == ResponseStatus.Ok) + { + _balance = balanceResponse.Data; + } + APIResponse> operationsResponse = await _transfersService.GetTransfers(); + if (operationsResponse.Status == ResponseStatus.Ok) + { + _operations = operationsResponse.Data; + } + APIResponse> accountResponse = await _accountsService.GetAccounts(id: _claims.AccountId); + if (accountResponse.Status == ResponseStatus.Ok && accountResponse.Data.Count() == 1) + { + _account = accountResponse.Data.First(); + } + + _dataLoadedState = balanceResponse.Status == ResponseStatus.Ok && operationsResponse.Status == ResponseStatus.Ok && accountResponse.Status == ResponseStatus.Ok ? DataLoadState.Loaded : DataLoadState.NotLoaded; + + StateHasChanged(); + } + } + } + + #endregion +} \ No newline at end of file diff --git a/SecureBank/Components/_Imports.razor b/SecureBank/Components/_Imports.razor index 66dec19..e29e44b 100644 --- a/SecureBank/Components/_Imports.razor +++ b/SecureBank/Components/_Imports.razor @@ -1,13 +1,18 @@ @using System.Net.Http @using System.Net.Http.Json +@using System.Net.Http.Headers; @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Authorization @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using SecureBank +@using SecureBank.Authentication @using SecureBank.Components -@using SecureBank.Website.Services; -@using SecureBank.Common; -@using SecureBank.Common.Accounts; +@using SecureBank.Website.Authentication +@using SecureBank.Website.Services +@using SecureBank.Common +@using SecureBank.Common.Accounts +@using SecureBank.Common.Transfers diff --git a/SecureBank/Program.cs b/SecureBank/Program.cs index b52156e..fa9fb30 100644 --- a/SecureBank/Program.cs +++ b/SecureBank/Program.cs @@ -1,10 +1,12 @@ using Blazored.SessionStorage; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using SecureBank.API.Authentication; +using SecureBank.API.Encryption; using SecureBank.Components; using SecureBank.Database; using SecureBank.Website.API; @@ -54,8 +56,10 @@ public class Program if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto + }); } else { @@ -68,13 +72,14 @@ public class Program x.SwaggerEndpoint("/api/swagger/v1/swagger.json", "SecureBank API"); x.RoutePrefix = "api/swagger"; }); + app.UseHttpsRedirection(); } - app.UseHttpsRedirection(); - app.UseStaticFiles(); app.UseAntiforgery(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + app.UseAuthentication(); app.UseAuthorization(); @@ -83,6 +88,17 @@ public class Program app.MapRazorComponents() .AddInteractiveServerRenderMode(); + using (var scope = app.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + var context = services.GetRequiredService(); + if (context.Database.GetPendingMigrations().Any()) + { + context.Database.Migrate(); + } + } + app.Run(); } @@ -136,12 +152,16 @@ public class Program // Configurations _builder.Services.AddSingleton(); + _builder.Services.AddSingleton(); // Helpers _builder.Services.AddSingleton(); + _builder.Services.AddSingleton(); // Services _builder.Services.AddSingleton(); + _builder.Services.AddSingleton(); + _builder.Services.AddSingleton(); } protected static void BuildWebsite() @@ -166,6 +186,8 @@ public class Program // Services _builder.Services.AddSingleton(); + _builder.Services.AddSingleton(); + _builder.Services.AddSingleton(); } #endregion diff --git a/SecureBank/Properties/launchSettings.json b/SecureBank/Properties/launchSettings.json index a2142ef..067c3e9 100644 --- a/SecureBank/Properties/launchSettings.json +++ b/SecureBank/Properties/launchSettings.json @@ -30,8 +30,7 @@ "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "environmentVariables": { - "ASPNETCORE_HTTPS_PORTS": "443", - "ASPNETCORE_HTTP_PORTS": "80" + "ASPNETCORE_HTTP_PORTS": "8080" }, "publishAllPorts": true, "useSSL": true diff --git a/SecureBank/SecureBank.csproj b/SecureBank/SecureBank.csproj index defa230..fd2f673 100644 --- a/SecureBank/SecureBank.csproj +++ b/SecureBank/SecureBank.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -6,7 +6,7 @@ enable 68c8748d-7175-410d-8bd6-a8ee07e58478 Linux - ..\Dockerfile + ..\Main.Dockerfile @@ -31,14 +31,22 @@ + + + + + + + + diff --git a/SecureBank/appsettings.Development.json b/SecureBank/appsettings.Development.json index 0c208ae..c599a22 100644 --- a/SecureBank/appsettings.Development.json +++ b/SecureBank/appsettings.Development.json @@ -1,8 +1,8 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } + "ConnectionStrings": { + "Default": "Data Source = ../database.db" + }, + "Endpoints": { + "Base": "https://localhost:7143/api" } } diff --git a/SecureBank/appsettings.Production.json b/SecureBank/appsettings.Production.json new file mode 100644 index 0000000..150abc4 --- /dev/null +++ b/SecureBank/appsettings.Production.json @@ -0,0 +1,8 @@ +{ + "ConnectionStrings": { + "Default": "Data Source = database.db" + }, + "Endpoints": { + "Base": "https://localhost/api" + } +} \ No newline at end of file diff --git a/SecureBank/appsettings.json b/SecureBank/appsettings.json index 8132383..d37f988 100644 --- a/SecureBank/appsettings.json +++ b/SecureBank/appsettings.json @@ -10,21 +10,37 @@ "Token": { "Key": "testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytest", "Issuer": "SecureBank", - "Audience": "access", + "Audience": "access", "Lifetime": "5" } }, - "ConnectionStrings": { - "Default": "Data Source = ../database.db" + "Encryption": { + "Key": "NG'MAjEJ'!'bZknqdME^9|nY?x|D$=9*", + "IV": "****************" }, "Endpoints": { - "Base": "https://localhost:7143/api", "Accounts": { "Base": "/accounts", "CreateAccount": "/create-account", + "ChangePassword": "/change-password", + "Authentication": "/authentication", "AuthenticationRefresh": "/authentication-refresh", "GetPasswordVariant": "/{0}/password-variant", - "Authentication": "/{0}/authentication" + "GetAccounts": "", + "ResetPassword": "/{0}/reset-password", + "UnlockAccount": "/{0}/unlock" + }, + "Balance": { + "Base": "/balance", + "GetAccountBalance": "/{0}", + "GetBalance": "" + }, + "Transfers": { + "Base": "/transfers", + "GetTransfers": "", + "GetUserTransfers": "/{0}", + "CreateAdminTransfer": "/admin-transfer", + "CreateUserTransfer": "/user-transfer" } } } diff --git a/SecureBank/wwwroot/app.css b/SecureBank/wwwroot/app.css index a12d372..d2fd004 100644 --- a/SecureBank/wwwroot/app.css +++ b/SecureBank/wwwroot/app.css @@ -56,4 +56,26 @@ h1:focus { .text-red { color: red; +} + +.text-green { + color: green; +} + +.text-big { + font-size: 40px; +} + +.vertical-center { + margin: 0; + position: absolute; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.table-brd { + border: 1px solid black; + border-collapse: collapse; + padding: 5px; } \ No newline at end of file diff --git a/database-default.db b/database-default.db new file mode 100644 index 0000000000000000000000000000000000000000..e035cc0ff716541f05f58e96e5f53c49f99b6373 GIT binary patch literal 69632 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 J>+AVb{|_e`{el1h literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..72cb238 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.7" + +services: + securebank-proxy: + container_name: securebank-proxy + build: + dockerfile: Nginx.Dockerfile + ports: + - "80:80" + - "443:443" + restart: always + + securebank-website: + container_name: securebank-website + depends_on: + - securebank-proxy + build: + dockerfile: Main.Dockerfile + expose: + - "8080:8080" + restart: always \ No newline at end of file