This commit is contained in:
2024-01-23 15:41:59 +01:00
Unverified
parent 5d5a69ccf7
commit 3b2b4c9b7e
76 changed files with 4100 additions and 888 deletions

2
.gitignore vendored
View File

@@ -398,4 +398,4 @@ FodyWeavers.xsd
*.sln.iml *.sln.iml
# SQLite database # SQLite database
*.db database.db

View File

@@ -1,9 +1,7 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app USER app
WORKDIR /app WORKDIR /app
EXPOSE 443 EXPOSE 8080
EXPOSE 80
ENV ASPNETCORE_URLS=https://+:443;http://+:80
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release 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.Common/SecureBank.Common.csproj", "SecureBank.Common/"]
COPY ["SecureBank.API/SecureBank.API.Services/SecureBank.API.Services.csproj", "SecureBank.API/SecureBank.API.Services/"] 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.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.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.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.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/"] COPY ["SecureBank.Website/SecureBank.Website.API/SecureBank.Website.API.csproj", "SecureBank.Website/SecureBank.Website.API/"]
RUN dotnet restore "./SecureBank/./SecureBank.csproj" 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 FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
COPY database-default.db database.db
USER root
ENTRYPOINT ["dotnet", "SecureBank.dll"] ENTRYPOINT ["dotnet", "SecureBank.dll"]

6
Nginx.Dockerfile Normal file
View File

@@ -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

31
Nginx/localhost.conf Normal file
View File

@@ -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

22
Nginx/localhost.crt Normal file
View File

@@ -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-----

28
Nginx/localhost.key Normal file
View File

@@ -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-----

43
Nginx/nginx.conf Normal file
View File

@@ -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;
}
}
}

View File

@@ -36,7 +36,7 @@ namespace SecureBank.API.Authentication
#region METHODS #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); DateTime expirationTime = DateTime.UtcNow.AddMinutes(_configuration.TokenLifetime);
@@ -44,11 +44,13 @@ namespace SecureBank.API.Authentication
{ {
Subject = new ClaimsIdentity(new List<Claim> Subject = new ClaimsIdentity(new List<Claim>
{ {
new Claim(JwtRegisteredClaimNames.Jti, tokenId.ToString()), new Claim("jti", tokenId.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, accountId.ToString()), new Claim("uid", account.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Exp, expirationTime.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("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, Expires = expirationTime,
Issuer = _configuration.TokenIssuer, Issuer = _configuration.TokenIssuer,

View File

@@ -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;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Identity.Client; using Microsoft.Identity.Client;
using SecureBank.API.Authentication;
using SecureBank.API.Services; using SecureBank.API.Services;
using SecureBank.Authentication;
using SecureBank.Common; using SecureBank.Common;
using SecureBank.Common.Accounts; using SecureBank.Common.Accounts;
using SecureBank.Helpers.Attributes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType; using static System.Runtime.InteropServices.JavaScript.JSType;
@@ -40,18 +46,17 @@ namespace SecureBank.API.Controllers
[HttpPost] [HttpPost]
[Route("create-account")] [Route("create-account")]
[AllowAnonymous] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[RequiresClaim("admin", "True")]
public async Task<ActionResult<APIResponse<int>>> CreateAccount([FromBody] CreateAccountRequest data) public async Task<ActionResult<APIResponse<int>>> CreateAccount([FromBody] CreateAccountRequest data)
{ {
APIResponse<int> response = await _accountsService.CreateAccount(data); APIResponse<int> response = await _accountsService.CreateAccount(data);
if (response.Success) return response.Status switch
{ {
return Ok(response); ResponseStatus.Ok => Ok(response),
} ResponseStatus.BadRequest => BadRequest(response),
else ResponseStatus.Unauthorized => Unauthorized(response),
{ };
return BadRequest(response);
}
} }
[HttpGet] [HttpGet]
@@ -60,35 +65,102 @@ namespace SecureBank.API.Controllers
public async Task<ActionResult<APIResponse<GetPasswordVariantResponse>>> GetPasswordVariant([FromRoute(Name = "account_id")] int accountId) public async Task<ActionResult<APIResponse<GetPasswordVariantResponse>>> GetPasswordVariant([FromRoute(Name = "account_id")] int accountId)
{ {
APIResponse<GetPasswordVariantResponse> response = await _accountsService.GetPasswordVariant(accountId); APIResponse<GetPasswordVariantResponse> response = await _accountsService.GetPasswordVariant(accountId);
if (response.Success) return response.Status switch
{ {
return Ok(response); ResponseStatus.Ok => Ok(response),
} ResponseStatus.BadRequest => BadRequest(response),
else ResponseStatus.Unauthorized => Unauthorized(response),
{ };
return BadRequest(response);
}
} }
[HttpPost] [HttpPost]
[Route("{account_id}/authentication")] [Route("authentication")]
[AllowAnonymous] [AllowAnonymous]
/* /*
* Action codes: * Action codes:
* 1 - Go back to client code input * 1 - Go back to client code input
* 2 - Failed login count increment * 2 - Change password required
*/ */
public async Task<ActionResult<APIResponse<string>>> Authentication([FromRoute(Name = "account_id")] int accountId, [FromBody] AuthenticationRequest data) public async Task<ActionResult<APIResponse<string>>> Authentication([FromBody] AuthenticationRequest data)
{ {
APIResponse<string> response = await _accountsService.Authentication(accountId, data); APIResponse<string> response = await _accountsService.Authentication(data);
if (response.Success) return response.Status switch
{ {
return Ok(response); ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
} }
else
[HttpPost]
[Route("authentication-refresh")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse<string>>> AuthenticationRefresh()
{ {
return BadRequest(response); APIResponse<string> response = await _accountsService.AuthenticationRefresh(new Claims(User.Claims));
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
} }
[HttpPatch]
[Route("change-password")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse>> 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<ActionResult<APIResponse<IEnumerable<AccountResponse>>>> GetAccounts([FromQuery]int? id, [FromQuery] string? iban)
{
APIResponse<IEnumerable<AccountResponse>> 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<ActionResult<APIResponse>> 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<ActionResult<APIResponse>> 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 #endregion

View File

@@ -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<ActionResult<APIResponse<decimal>>> GetAccountBalance([FromRoute(Name = "account_id")]int accountId)
{
APIResponse<decimal> 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<ActionResult<APIResponse<decimal>>> GetBalance()
{
APIResponse<decimal> 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
}
}

View File

@@ -7,12 +7,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\SecureBank.Authentication\SecureBank.Authentication.csproj" />
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" /> <ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\SecureBank.API.Services\SecureBank.API.Services.csproj" /> <ProjectReference Include="..\SecureBank.API.Services\SecureBank.API.Services.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -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<ActionResult<APIResponse<IEnumerable<TransferResponse>>>> GetTransfers()
{
APIResponse<IEnumerable<TransferResponse>> 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<ActionResult<APIResponse<IEnumerable<TransferResponse>>>> GetUserTransfers([FromRoute(Name = "account_id")]int accountId)
{
APIResponse<IEnumerable<TransferResponse>> 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<ActionResult<APIResponse>> 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<ActionResult<APIResponse>> 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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -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
}
}

View File

@@ -6,8 +6,17 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" /> <ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Enums\" />
</ItemGroup>
</Project> </Project>

View File

@@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SecureBank.API.Helpers; using SecureBank.API.Helpers;
using SecureBank.API.Authentication; using SecureBank.API.Authentication;
using SecureBank.Authentication;
using SecureBank.Common; using SecureBank.Common;
using SecureBank.Common.Accounts; using SecureBank.Common.Accounts;
using SecureBank.Database; using SecureBank.Database;
@@ -15,6 +16,10 @@ using System.Runtime.Intrinsics.Arm;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SecureBank.API.Encryption;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.Identity.Client;
namespace SecureBank.API.Services namespace SecureBank.API.Services
{ {
@@ -22,7 +27,12 @@ namespace SecureBank.API.Services
{ {
Task<APIResponse<int>> CreateAccount(CreateAccountRequest data); Task<APIResponse<int>> CreateAccount(CreateAccountRequest data);
Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId); Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId);
Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data); Task<APIResponse<string>> Authentication(AuthenticationRequest data);
Task<APIResponse<string>> AuthenticationRefresh(Claims claims);
Task<APIResponse> ChangePassword(Claims claims, ChangePasswordRequest data);
Task<APIResponse<IEnumerable<AccountResponse>>> GetAccounts(string? iban, int? id, Claims claims);
Task<APIResponse> ResetPassword(int accountId);
Task<APIResponse> UnlockAccount(int accountId);
} }
@@ -33,6 +43,8 @@ namespace SecureBank.API.Services
private AuthenticationHelper _authenticationHelper; private AuthenticationHelper _authenticationHelper;
private EncryptionHelper _encryptionHelper;
private DatabaseContext _database; private DatabaseContext _database;
private ILogger<AccountsService> _logger; private ILogger<AccountsService> _logger;
@@ -43,12 +55,11 @@ namespace SecureBank.API.Services
#region CONSTRUCTORS #region CONSTRUCTORS
public AccountsService(AuthenticationHelper authenticationHelper, DatabaseContext database, ILogger<AccountsService> logger) public AccountsService(AuthenticationHelper authenticationHelper, EncryptionHelper encryptionHelper, DatabaseContext database, ILogger<AccountsService> logger)
{ {
_authenticationHelper = authenticationHelper; _authenticationHelper = authenticationHelper;
_encryptionHelper = encryptionHelper;
_database = database; _database = database;
_logger = logger; _logger = logger;
} }
@@ -103,6 +114,31 @@ namespace SecureBank.API.Services
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.PhoneNumber)), CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.PhoneNumber)),
Message = "Phone number cannot be empty" Message = "Phone number cannot be empty"
}, },
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.Address)),
Message = "Address cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.PESEL)),
Message = "PESEL cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => x.PESEL.Length != 11),
Message = "PESEL must be 11 charaters long"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.IdCardNumber)),
Message = "Id card number cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => x.IdCardNumber.Length != 9),
Message = "Id card number must be 9 characters long"
},
}; };
foreach (Check<CreateAccountRequest> check in checks) foreach (Check<CreateAccountRequest> check in checks)
@@ -112,21 +148,44 @@ namespace SecureBank.API.Services
return new APIResponse<int> return new APIResponse<int>
{ {
Message = check.Message, 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 Account account = new Account
{ {
FirstName = data.FirstName, FirstName = data.FirstName,
LastName = data.LastName, LastName = data.LastName,
Email = data.Email, Email = data.Email,
PhoneNumber = data.PhoneNumber.Replace(" ", string.Empty), 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.Accounts.AddAsync(account);
await _database.SaveChangesAsync(); 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(); string password = GeneratePassword();
await GeneratePasswordVariants(password, account.Id); await GeneratePasswordVariants(password, account.Id);
@@ -136,8 +195,7 @@ namespace SecureBank.API.Services
return new APIResponse<int> return new APIResponse<int>
{ {
Data = account.Id, Data = account.Id
Success = true
}; };
} }
@@ -148,7 +206,7 @@ namespace SecureBank.API.Services
{ {
return new APIResponse<GetPasswordVariantResponse> return new APIResponse<GetPasswordVariantResponse>
{ {
Success = false, Status = ResponseStatus.BadRequest,
Message = $"Account does not exists" Message = $"Account does not exists"
}; };
} }
@@ -157,7 +215,7 @@ namespace SecureBank.API.Services
{ {
return new APIResponse<GetPasswordVariantResponse> return new APIResponse<GetPasswordVariantResponse>
{ {
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." 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<GetPasswordVariantResponse> return new APIResponse<GetPasswordVariantResponse>
{ {
Success = false, Status = ResponseStatus.BadRequest,
Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account." 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<GetPasswordVariantResponse> return new APIResponse<GetPasswordVariantResponse>
{ {
Success = true,
Data = new GetPasswordVariantResponse Data = new GetPasswordVariantResponse
{ {
LoginRequestId = loginRequest.Id, LoginRequestId = loginRequest.Id,
@@ -198,26 +255,15 @@ namespace SecureBank.API.Services
}; };
} }
public async Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data) public async Task<APIResponse<string>> Authentication(AuthenticationRequest data)
{ {
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<string>
{
Success = false,
Message = $"Account does not exists"
};
}
AccountLoginRequest? loginRequest = await _database.AccountLoginRequests.FirstOrDefaultAsync(x => x.Id == data.LoginRequestId); AccountLoginRequest? loginRequest = await _database.AccountLoginRequests.FirstOrDefaultAsync(x => x.Id == data.LoginRequestId);
if (loginRequest is null) if (loginRequest is null)
{ {
return new APIResponse<string> return new APIResponse<string>
{ {
Success = false, Status = ResponseStatus.BadRequest,
Message = $"Login request does not exist" Message = $"Login request does not exist"
}; };
} }
@@ -226,33 +272,17 @@ namespace SecureBank.API.Services
Account loginRequestAccount = password.Account; Account loginRequestAccount = password.Account;
if (loginRequestAccount.Id != account.Id) APIResponse<string>? 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."; return accountCheck;
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<string>
{
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<string>
{
Success = false,
Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account."
};
} }
if (loginRequest.ValidTo < DateTime.Now) if (loginRequest.ValidTo < DateTime.Now)
{ {
return new APIResponse<string> return new APIResponse<string>
{ {
Success = false, Status = ResponseStatus.BadRequest,
ActionCode = 1, ActionCode = 1,
Message = $"Login request has expired. Go back and try again." Message = $"Login request has expired. Go back and try again."
}; };
@@ -261,25 +291,199 @@ namespace SecureBank.API.Services
byte[] passwordDb = password.Password; byte[] passwordDb = password.Password;
byte[] passwordProvided = HashPassword(data.Password, password.LeftSalt, password.RightSalt); 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(); await _database.SaveChangesAsync();
return new APIResponse<string> return new APIResponse<string>
{ {
Success = false, Status = ResponseStatus.BadRequest,
ActionCode = 2,
Message = $"Incorrect password" 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<string> return new APIResponse<string>
{ {
Data = token, ActionCode = loginRequestAccount.TemporaryPassword ? 2 : 0,
Success = true, Data = token
};
}
public async Task<APIResponse<string>> AuthenticationRefresh(Claims claims)
{
if (claims.IsOneTimeToken)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"One time token cannot be refreshed."
};
}
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == claims.AccountId);
APIResponse<string>? accountCheck = CheckAccount(account);
if (accountCheck is not null)
{
return accountCheck;
}
string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), account, false);
return new APIResponse<string>
{
Data = token
};
}
public async Task<APIResponse> 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<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists"
};
}
IEnumerable<string> 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<AccountPasswordIndex> indexes = await _database.AccountPasswordIndexes.Where(x => x.AccountPassword.AccountId == claims.AccountId).ToListAsync();
_database.AccountPasswordIndexes.AttachRange(indexes);
_database.AccountPasswordIndexes.RemoveRange(indexes);
await _database.SaveChangesAsync();
IEnumerable<AccountPassword> 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<APIResponse<IEnumerable<AccountResponse>>> GetAccounts(string? iban, int? id, Claims claims)
{
IEnumerable<Account> 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<IEnumerable<AccountResponse>>
{
Status = ResponseStatus.Unauthorized,
Message = $"You don't have permission to get information about accounts that aren't yours"
};
}
List<AccountResponse> data = new List<AccountResponse>();
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<IEnumerable<AccountResponse>>
{
Data = data
};
}
public async Task<APIResponse> ResetPassword(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists"
};
}
await PasswordReset(account);
return new APIResponse<int>
{
Data = account.Id
};
}
public async Task<APIResponse> UnlockAccount(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists"
};
}
await PasswordReset(account);
account.LockReason = null;
account.LoginFailedCount = 0;
await _database.SaveChangesAsync();
return new APIResponse<int>
{
Data = account.Id
}; };
} }
@@ -289,6 +493,60 @@ namespace SecureBank.API.Services
#region PRIVATE METHODS #region PRIVATE METHODS
protected async Task PasswordReset(Account account)
{
IEnumerable<AccountPasswordIndex> indexes = await _database.AccountPasswordIndexes.Where(x => x.AccountPassword.AccountId == account.Id).ToListAsync();
_database.AccountPasswordIndexes.AttachRange(indexes);
_database.AccountPasswordIndexes.RemoveRange(indexes);
await _database.SaveChangesAsync();
IEnumerable<AccountPassword> 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<string>? CheckAccount(Account? account)
{
if (account is null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists."
};
}
if (account.LockReason is not null)
{
return new APIResponse<string>
{
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<string>
{
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) protected byte[] HashPassword(string password, string leftSalt, string rightSalt)
{ {
SHA512 sha = SHA512.Create(); SHA512 sha = SHA512.Create();
@@ -355,11 +613,16 @@ namespace SecureBank.API.Services
protected IEnumerable<string> CheckPassword(string password) protected IEnumerable<string> CheckPassword(string password)
{ {
int minLength = 8; int minLength = 8;
uint maxLength = 20;
if (password.Length < minLength) if (password.Length < minLength)
{ {
yield return $"Password must be at least {minLength} characters long"; 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))) if (!password.Any(x => Char.IsUpper(x)))
{ {
yield return $"Password must contain at least one uppercase character"; 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"; 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"; yield return $"Password must contain at least one special character";
} }

View File

@@ -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<APIResponse<decimal>> GetAccountBalance(int accountId);
Task<APIResponse<decimal>> 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<APIResponse<decimal>> GetBalance(Claims claims) => await GetAccountBalance(claims.AccountId);
public async Task<APIResponse<decimal>> GetAccountBalance(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<decimal>
{
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<decimal>
{
Data = 0 + transfersIncoming.Sum(x => x.Amount) - transfersOutcoming.Sum(x => x.Amount),
};
}
#endregion
#region PRIVATE METHODS
#endregion
}
}

View File

@@ -7,10 +7,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Authentication\SecureBank.Authentication.csproj" />
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" /> <ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" /> <ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" />
<ProjectReference Include="..\..\SecureBank.Extensions\SecureBank.Extensions.csproj" /> <ProjectReference Include="..\..\SecureBank.Extensions\SecureBank.Extensions.csproj" />
<ProjectReference Include="..\SecureBank.API.Authentication\SecureBank.API.Authentication.csproj" /> <ProjectReference Include="..\SecureBank.API.Authentication\SecureBank.API.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.API.Encryption\SecureBank.API.Encryption.csproj" />
<ProjectReference Include="..\SecureBank.API.Helpers\SecureBank.API.Helpers.csproj" /> <ProjectReference Include="..\SecureBank.API.Helpers\SecureBank.API.Helpers.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -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<APIResponse<IEnumerable<TransferResponse>>> GetTransfers(Claims claims);
Task<APIResponse<IEnumerable<TransferResponse>>> GetUserTransfers(int accountId);
Task<APIResponse> CreateAdminTransfer(CreateAdminTransferRequest data);
Task<APIResponse> 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<APIResponse<IEnumerable<TransferResponse>>> GetTransfers(Claims claims) => await GetUserTransfers(claims.AccountId);
public async Task<APIResponse<IEnumerable<TransferResponse>>> GetUserTransfers(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<IEnumerable<TransferResponse>>
{
Status = ResponseStatus.BadRequest,
Message = "Account does not exists"
};
}
string iban = account.IBAN;
List<TransferResponse> list = new List<TransferResponse>();
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<IEnumerable<TransferResponse>>
{
Data = list
};
}
public async Task<APIResponse> CreateAdminTransfer(CreateAdminTransferRequest data)
{
Check<CreateAdminTransferRequest>[] checks = new Check<CreateAdminTransferRequest>[]
{
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x is null),
Message = "Body cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.SenderAccountNumber is null),
Message = "Sender account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => !x.SenderAccountNumber.All(y => char.IsDigit(y))),
Message = "Wrong sender account number format. Account number consists only of digits"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.SenderAccountNumber.Length != 26),
Message = "Sender account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.ReceiverAccountNumber is null),
Message = "Receiver account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => !x.ReceiverAccountNumber.All(y => char.IsDigit(y))),
Message = "Wrong receiver account number format. Account number consists only of digits"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.ReceiverAccountNumber.Length != 26),
Message = "Receiver account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.Amount <= 0),
Message = "Receiver account number cannot be empty"
},
};
foreach (Check<CreateAdminTransferRequest> 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<APIResponse> CreateUserTransfer(CreateUserTransferRequest data, Claims claims)
{
Check<CreateUserTransferRequest>[] checks = new Check<CreateUserTransferRequest>[]
{
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x is null),
Message = "Body cannot be empty"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x.ReceiverAccountNumber is null),
Message = "Receiver account number cannot be empty"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => !x.ReceiverAccountNumber.All(y => char.IsDigit(y))),
Message = "Wrong receiver account number format. Account number consists only of digits"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x.ReceiverAccountNumber.Length != 26),
Message = "Receiver account number cannot be empty"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x.Amount <= 0),
Message = "Receiver account number cannot be empty"
},
};
foreach (Check<CreateUserTransferRequest> 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<IEnumerable<TransferResponse>>
{
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
}
}

View File

@@ -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<Claim> 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
}
}

View File

@@ -12,21 +12,21 @@ namespace SecureBank.Common
{ {
[JsonProperty("message")] [JsonProperty("message")]
[JsonPropertyName("message")] [JsonPropertyName("message")]
public string Message { get; set; } public string? Message { get; set; }
[JsonProperty("success")] [JsonProperty("status")]
[JsonPropertyName("success")] [JsonPropertyName("status")]
public bool Success { get; set; } public ResponseStatus Status { get; set; } = ResponseStatus.Ok;
[JsonProperty("action_code")] [JsonProperty("action_code")]
[JsonPropertyName("action_code")] [JsonPropertyName("action_code")]
public int ActionCode { get; set; } public int? ActionCode { get; set; }
} }
public class APIResponse<T> : APIResponse public class APIResponse<T> : APIResponse
{ {
[JsonProperty("data")] [JsonProperty("data")]
[JsonPropertyName("data")] [JsonPropertyName("data")]
public T Data { get; set; } public T? Data { get; set; }
} }
} }

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -25,5 +25,17 @@ namespace SecureBank.Common.Accounts
[JsonProperty("phone_number")] [JsonProperty("phone_number")]
[JsonPropertyName("phone_number")] [JsonPropertyName("phone_number")]
public string PhoneNumber { get; set; } 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; }
} }
} }

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -30,6 +30,37 @@ namespace SecureBank.Database
[MaxLength(20)] [MaxLength(20)]
public string PhoneNumber { get; set; } 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] [Required]
public byte LoginFailedCount { get; set; } = 0; public byte LoginFailedCount { get; set; } = 0;

View File

@@ -25,6 +25,7 @@ namespace SecureBank.Database
public virtual DbSet<AccountPassword> AccountPasswords { get; set; } public virtual DbSet<AccountPassword> AccountPasswords { get; set; }
public virtual DbSet<AccountPasswordIndex> AccountPasswordIndexes { get; set; } public virtual DbSet<AccountPasswordIndex> AccountPasswordIndexes { get; set; }
public virtual DbSet<AccountLoginRequest> AccountLoginRequests { get; set; } public virtual DbSet<AccountLoginRequest> AccountLoginRequests { get; set; }
public virtual DbSet<Transfer> Transfers { get; set; }
#endregion #endregion

View File

@@ -1,132 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<byte>("LoginFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<bool>("TemporaryPassword")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Accounts");
});
modelBuilder.Entity("SecureBank.Database.AccountPassword", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccountId")
.HasColumnType("INTEGER");
b.Property<string>("LeftSalt")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<byte[]>("Password")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("BLOB");
b.Property<string>("RightSalt")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("AccountPasswords");
});
modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b =>
{
b.Property<long>("AccountPasswordId")
.HasColumnType("INTEGER");
b.Property<byte>("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
}
}
}

View File

@@ -1,138 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<byte>("LoginFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<bool>("TemporaryPassword")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Accounts");
});
modelBuilder.Entity("SecureBank.Database.AccountPassword", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccountId")
.HasColumnType("INTEGER");
b.Property<string>("LeftSalt")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<byte[]>("Password")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("BLOB");
b.Property<string>("RightSalt")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("AccountPasswords");
});
modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<long>("AccountPasswordId")
.HasColumnType("INTEGER");
b.Property<byte>("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
}
}
}

View File

@@ -1,39 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SecureBank.Database.Migrations
{
/// <inheritdoc />
public partial class Migration2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "Id",
table: "AccountPasswordIndexes",
type: "INTEGER",
nullable: false,
defaultValue: 0L)
.Annotation("Sqlite:Autoincrement", true);
migrationBuilder.AddPrimaryKey(
name: "PK_AccountPasswordIndexes",
table: "AccountPasswordIndexes",
column: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_AccountPasswordIndexes",
table: "AccountPasswordIndexes");
migrationBuilder.DropColumn(
name: "Id",
table: "AccountPasswordIndexes");
}
}
}

View File

@@ -1,169 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<byte>("LoginFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<bool>("TemporaryPassword")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Accounts");
});
modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<long>("AccountPasswordId")
.HasColumnType("INTEGER");
b.Property<DateTime>("ValidTo")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountPasswordId");
b.ToTable("AccountLoginRequests");
});
modelBuilder.Entity("SecureBank.Database.AccountPassword", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccountId")
.HasColumnType("INTEGER");
b.Property<string>("LeftSalt")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<byte[]>("Password")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("BLOB");
b.Property<string>("RightSalt")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("AccountPasswords");
});
modelBuilder.Entity("SecureBank.Database.AccountPasswordIndex", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<long>("AccountPasswordId")
.HasColumnType("INTEGER");
b.Property<byte>("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
}
}
}

View File

@@ -1,46 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SecureBank.Database.Migrations
{
/// <inheritdoc />
public partial class Migration3 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AccountLoginRequests",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
AccountPasswordId = table.Column<long>(type: "INTEGER", nullable: false),
ValidTo = table.Column<DateTime>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AccountLoginRequests");
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SecureBank.Database.Migrations
{
/// <inheritdoc />
public partial class Migration4 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "LockReason",
table: "Accounts",
type: "TEXT",
maxLength: 1000,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LockReason",
table: "Accounts");
}
}
}

View File

@@ -11,8 +11,8 @@ using SecureBank.Database;
namespace SecureBank.Database.Migrations namespace SecureBank.Database.Migrations
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(DatabaseContext))]
[Migration("20240115132220_Migration4")] [Migration("20240122225115_Migration1")]
partial class Migration4 partial class Migration1
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -26,6 +26,26 @@ namespace SecureBank.Database.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<byte[]>("CardCVV")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<byte[]>("CardExpirationDate")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<byte[]>("CardNumber")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("BLOB");
b.Property<string>("Email") b.Property<string>("Email")
.IsRequired() .IsRequired()
.HasMaxLength(300) .HasMaxLength(300)
@@ -36,6 +56,19 @@ namespace SecureBank.Database.Migrations
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("IBAN")
.IsRequired()
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<byte[]>("IdCardNumber")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<bool>("IsAdmin")
.HasColumnType("INTEGER");
b.Property<string>("LastName") b.Property<string>("LastName")
.IsRequired() .IsRequired()
.HasMaxLength(100) .HasMaxLength(100)
@@ -48,6 +81,11 @@ namespace SecureBank.Database.Migrations
b.Property<byte>("LoginFailedCount") b.Property<byte>("LoginFailedCount")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<byte[]>("PESEL")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<string>("PhoneNumber") b.Property<string>("PhoneNumber")
.IsRequired() .IsRequired()
.HasMaxLength(20) .HasMaxLength(20)
@@ -130,6 +168,55 @@ namespace SecureBank.Database.Migrations
b.ToTable("AccountPasswordIndexes"); b.ToTable("AccountPasswordIndexes");
}); });
modelBuilder.Entity("SecureBank.Database.Transfer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<decimal>("Amount")
.HasPrecision(14, 2)
.HasColumnType("TEXT");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<string>("ReceiverAccountNumber")
.IsRequired()
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<string>("ReceiverAddress")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ReceiverName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SenderAccountNumber")
.IsRequired()
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<string>("SenderAddress")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SenderName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Transfers");
});
modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b => modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b =>
{ {
b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword")

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations; using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
@@ -20,14 +21,43 @@ namespace SecureBank.Database.Migrations
LastName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false), LastName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Email = table.Column<string>(type: "TEXT", maxLength: 300, nullable: false), Email = table.Column<string>(type: "TEXT", maxLength: 300, nullable: false),
PhoneNumber = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false), PhoneNumber = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
Address = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
PESEL = table.Column<byte[]>(type: "BLOB", maxLength: 16, nullable: false),
IdCardNumber = table.Column<byte[]>(type: "BLOB", maxLength: 16, nullable: false),
IBAN = table.Column<string>(type: "TEXT", maxLength: 26, nullable: false),
CardNumber = table.Column<byte[]>(type: "BLOB", maxLength: 32, nullable: false),
CardExpirationDate = table.Column<byte[]>(type: "BLOB", maxLength: 16, nullable: false),
CardCVV = table.Column<byte[]>(type: "BLOB", maxLength: 16, nullable: false),
IsAdmin = table.Column<bool>(type: "INTEGER", nullable: false),
LoginFailedCount = table.Column<byte>(type: "INTEGER", nullable: false), LoginFailedCount = table.Column<byte>(type: "INTEGER", nullable: false),
TemporaryPassword = table.Column<bool>(type: "INTEGER", nullable: false) TemporaryPassword = table.Column<bool>(type: "INTEGER", nullable: false),
LockReason = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Accounts", x => x.Id); table.PrimaryKey("PK_Accounts", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Transfers",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
SenderAccountNumber = table.Column<string>(type: "TEXT", maxLength: 26, nullable: false),
SenderName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
SenderAddress = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ReceiverAccountNumber = table.Column<string>(type: "TEXT", maxLength: 26, nullable: false),
ReceiverName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ReceiverAddress = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Amount = table.Column<decimal>(type: "TEXT", precision: 14, scale: 2, nullable: false),
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
Date = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Transfers", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AccountPasswords", name: "AccountPasswords",
columns: table => new columns: table => new
@@ -50,15 +80,37 @@ namespace SecureBank.Database.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "AccountLoginRequests",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
AccountPasswordId = table.Column<long>(type: "INTEGER", nullable: false),
ValidTo = table.Column<DateTime>(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( migrationBuilder.CreateTable(
name: "AccountPasswordIndexes", name: "AccountPasswordIndexes",
columns: table => new columns: table => new
{ {
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AccountPasswordId = table.Column<long>(type: "INTEGER", nullable: false), AccountPasswordId = table.Column<long>(type: "INTEGER", nullable: false),
Index = table.Column<byte>(type: "INTEGER", nullable: false) Index = table.Column<byte>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_AccountPasswordIndexes", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_AccountPasswordIndexes_AccountPasswords_AccountPasswordId", name: "FK_AccountPasswordIndexes_AccountPasswords_AccountPasswordId",
column: x => x.AccountPasswordId, column: x => x.AccountPasswordId,
@@ -67,6 +119,11 @@ namespace SecureBank.Database.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_AccountLoginRequests_AccountPasswordId",
table: "AccountLoginRequests",
column: "AccountPasswordId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AccountPasswordIndexes_AccountPasswordId", name: "IX_AccountPasswordIndexes_AccountPasswordId",
table: "AccountPasswordIndexes", table: "AccountPasswordIndexes",
@@ -81,9 +138,15 @@ namespace SecureBank.Database.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable(
name: "AccountLoginRequests");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AccountPasswordIndexes"); name: "AccountPasswordIndexes");
migrationBuilder.DropTable(
name: "Transfers");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AccountPasswords"); name: "AccountPasswords");

View File

@@ -23,6 +23,26 @@ namespace SecureBank.Database.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<byte[]>("CardCVV")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<byte[]>("CardExpirationDate")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<byte[]>("CardNumber")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("BLOB");
b.Property<string>("Email") b.Property<string>("Email")
.IsRequired() .IsRequired()
.HasMaxLength(300) .HasMaxLength(300)
@@ -33,6 +53,19 @@ namespace SecureBank.Database.Migrations
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("IBAN")
.IsRequired()
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<byte[]>("IdCardNumber")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<bool>("IsAdmin")
.HasColumnType("INTEGER");
b.Property<string>("LastName") b.Property<string>("LastName")
.IsRequired() .IsRequired()
.HasMaxLength(100) .HasMaxLength(100)
@@ -45,6 +78,11 @@ namespace SecureBank.Database.Migrations
b.Property<byte>("LoginFailedCount") b.Property<byte>("LoginFailedCount")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<byte[]>("PESEL")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("BLOB");
b.Property<string>("PhoneNumber") b.Property<string>("PhoneNumber")
.IsRequired() .IsRequired()
.HasMaxLength(20) .HasMaxLength(20)
@@ -127,6 +165,55 @@ namespace SecureBank.Database.Migrations
b.ToTable("AccountPasswordIndexes"); b.ToTable("AccountPasswordIndexes");
}); });
modelBuilder.Entity("SecureBank.Database.Transfer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<decimal>("Amount")
.HasPrecision(14, 2)
.HasColumnType("TEXT");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<string>("ReceiverAccountNumber")
.IsRequired()
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<string>("ReceiverAddress")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ReceiverName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SenderAccountNumber")
.IsRequired()
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<string>("SenderAddress")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SenderName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Transfers");
});
modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b => modelBuilder.Entity("SecureBank.Database.AccountLoginRequest", b =>
{ {
b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword") b.HasOne("SecureBank.Database.AccountPassword", "AccountPassword")

View File

@@ -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
}
}

View File

@@ -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<Claim> 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<string, object>? keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
if (keyValuePairs is null)
{
throw new Exception("Incorrect token");
}
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
}
#endregion
}
}

View File

@@ -1,6 +1,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using SecureBank.Common; using SecureBank.Common;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -31,28 +32,32 @@ namespace SecureBank.Website.API
#region PUBLIC METHODS #region PUBLIC METHODS
public async Task<APIResponse<TResponse>> SendAsync<TResponse>(APIMethodType type, string url) public async Task<APIResponse<TResponse>> SendAsync<TResponse>(APIMethodType type, string url, Dictionary<string, string>? query = null)
{ {
return await SendRequestAsync<APIResponse<TResponse>>(type, url, null); url = AddQuery(url, query);
return await SendRequestAndParseBodyAsync<TResponse>(type, url, null);
} }
public async Task<APIResponse> SendAsync(APIMethodType type, string url) public async Task<APIResponse> SendAsync(APIMethodType type, string url, Dictionary<string, string>? query = null)
{ {
return await SendRequestAsync<APIResponse>(type, url, null); url = AddQuery(url, query);
return await SendRequestAndParseBodyAsync(type, url, null);
} }
public async Task<APIResponse<TResponse>> SendAsync<TResponse, TBody>(APIMethodType type, string url, TBody body) public async Task<APIResponse<TResponse>> SendAsync<TResponse, TBody>(APIMethodType type, string url, TBody body, Dictionary<string, string>? query = null)
{ {
url = AddQuery(url, query);
HttpContent content = PrepareBody(body); HttpContent content = PrepareBody(body);
return await SendRequestAsync<APIResponse<TResponse>>(type, url, content); return await SendRequestAndParseBodyAsync<TResponse>(type, url, content);
} }
public async Task<APIResponse> SendAsync<TBody>(APIMethodType type, string url, TBody body) public async Task<APIResponse> SendAsync<TBody>(APIMethodType type, string url, TBody body, Dictionary<string, string>? query = null)
{ {
url = AddQuery(url, query);
HttpContent content = PrepareBody(body); HttpContent content = PrepareBody(body);
return await SendRequestAsync<APIResponse>(type, url, content); return await SendRequestAndParseBodyAsync(type, url, content);
} }
#endregion #endregion
@@ -61,6 +66,25 @@ namespace SecureBank.Website.API
#region PRIVATE METHODS #region PRIVATE METHODS
private string AddQuery(string url, Dictionary<string, string>? query)
{
if (query is not null && query.Count > 0)
{
Dictionary<string, string> queryNew = query.ToDictionary();
StringBuilder sb = new StringBuilder(url);
KeyValuePair<string, string> item = queryNew.ElementAt(0);
queryNew.Remove(item.Key);
sb.Append($"?{item.Key}={item.Value}");
foreach (KeyValuePair<string, string> item2 in queryNew)
{
sb.Append($"&{item2.Key}={item2.Value}");
}
return sb.ToString();
}
return url;
}
private HttpContent PrepareBody<T>(T body) private HttpContent PrepareBody<T>(T body)
{ {
string json = JsonConvert.SerializeObject(body); string json = JsonConvert.SerializeObject(body);
@@ -71,37 +95,103 @@ namespace SecureBank.Website.API
return content; return content;
} }
private async Task<T> SendRequestAsync<T>(APIMethodType type, string url, HttpContent? content) private async Task<APIResponse> SendRequestAndParseBodyAsync(APIMethodType type, string url, HttpContent? content)
{ {
try try
{ {
HttpResponseMessage response = type switch HttpResponseMessage response = await SendRequestAsync(type, url, content);
{
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()
};
string responseBodyString = await response.Content.ReadAsStringAsync(); string stringResponse = await response.Content.ReadAsStringAsync();
T? responseBodyObject = JsonConvert.DeserializeObject<T>(responseBodyString); APIResponse? responseBodyObject = JsonConvert.DeserializeObject<APIResponse>(stringResponse);
if (responseBodyObject is null) 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; return responseBodyObject;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine(ex); return new APIResponse
throw; {
Status = ResponseStatus.BadRequest,
Message = ex.Message
};
} }
} }
private async Task<APIResponse<T>> SendRequestAndParseBodyAsync<T>(APIMethodType type, string url, HttpContent? content)
{
try
{
HttpResponseMessage response = await SendRequestAsync(type, url, content);
string stringResponse = await response.Content.ReadAsStringAsync();
APIResponse<T>? responseBodyObject = JsonConvert.DeserializeObject<APIResponse<T>>(stringResponse);
if (responseBodyObject is null)
{
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
return new APIResponse<T>
{
Status = ResponseStatus.Unauthorized,
Message = $"You do not have permission"
};
}
else
{
return new APIResponse<T>
{
Status = ResponseStatus.BadRequest,
Message = $"Wrong response type. Response: {stringResponse}; {response.StatusCode}"
};
}
}
return responseBodyObject;
}
catch (Exception ex)
{
return new APIResponse<T>
{
Status = ResponseStatus.BadRequest,
Message = ex.Message
};
}
}
private async Task<HttpResponseMessage> 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 #endregion
} }
} }

View File

@@ -16,9 +16,25 @@ namespace SecureBank.Website.API
// Accounts // Accounts
public string AccountsBase { get; private set; } public string AccountsBase { get; private set; }
public string AccountsCreateAccount { get; private set; } public string AccountsCreateAccount { get; private set; }
public string AccountsChangePassword { get; private set; }
public string AccountsGetPasswordVariant { get; private set; } public string AccountsGetPasswordVariant { get; private set; }
public string AccountsAuthentication { get; private set; } public string AccountsAuthentication { get; private set; }
public string AccountsAuthenticationRefresh { 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 #endregion
@@ -32,9 +48,23 @@ namespace SecureBank.Website.API
AccountsBase = $"{Base}{configuration.GetSection("Endpoints").GetSection("Accounts")["Base"]}"; AccountsBase = $"{Base}{configuration.GetSection("Endpoints").GetSection("Accounts")["Base"]}";
AccountsCreateAccount = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["CreateAccount"]}"; AccountsCreateAccount = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["CreateAccount"]}";
AccountsChangePassword = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["ChangePassword"]}";
AccountsGetPasswordVariant = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["GetPasswordVariant"]}"; AccountsGetPasswordVariant = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["GetPasswordVariant"]}";
AccountsAuthentication = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["Authentication"]}"; AccountsAuthentication = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["Authentication"]}";
AccountsAuthenticationRefresh = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["AuthenticationRefresh"]}"; 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 #endregion

View File

@@ -11,6 +11,7 @@ namespace SecureBank.Website.API
GET, GET,
POST, POST,
PUT, PUT,
PATCH,
DELETE DELETE
} }
} }

View File

@@ -9,10 +9,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0" /> <PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\SecureBank.Helpers\SecureBank.Helpers.csproj" />
<ProjectReference Include="..\SecureBank.Website.Services\SecureBank.Website.Services.csproj" /> <ProjectReference Include="..\SecureBank.Website.Services\SecureBank.Website.Services.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Security.Claims; using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -56,16 +57,22 @@ namespace SecureBank.Website.Authentication
APIResponse<string> refreshResponse = await _accountsService.AuthenticationRefresh(); APIResponse<string> refreshResponse = await _accountsService.AuthenticationRefresh();
if (!refreshResponse.Success) if (refreshResponse.Status != ResponseStatus.Ok)
{ {
await _authenticationHelper.RemoveToken();
_httpClient.DefaultRequestHeaders.Authorization = null; _httpClient.DefaultRequestHeaders.Authorization = null;
return state; return state;
} }
token = refreshResponse.Data; token = refreshResponse.Data;
await _authenticationHelper.SaveToken(token);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", 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; return state;
} }

View File

@@ -15,6 +15,10 @@ namespace SecureBank.Website.Services
Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId); Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId);
Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data); Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data);
Task<APIResponse<string>> AuthenticationRefresh(); Task<APIResponse<string>> AuthenticationRefresh();
Task<APIResponse> ChangePassword(ChangePasswordRequest data);
Task<APIResponse<IEnumerable<AccountResponse>>> GetAccounts(int? id = null, string? iban = null);
Task<APIResponse> ResetPassword(int accountId);
Task<APIResponse> UnlockAccount(int accountId);
} }
@@ -57,8 +61,7 @@ namespace SecureBank.Website.Services
public async Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data) public async Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data)
{ {
string url = string.Format(_configuration.AccountsAuthentication, accountId); return await _apiClient.SendAsync<string, AuthenticationRequest>(APIMethodType.POST, _configuration.AccountsAuthentication, data);
return await _apiClient.SendAsync<string, AuthenticationRequest>(APIMethodType.POST, url, data);
} }
public async Task<APIResponse<string>> AuthenticationRefresh() public async Task<APIResponse<string>> AuthenticationRefresh()
@@ -66,6 +69,37 @@ namespace SecureBank.Website.Services
return await _apiClient.SendAsync<string>(APIMethodType.POST, _configuration.AccountsAuthenticationRefresh); return await _apiClient.SendAsync<string>(APIMethodType.POST, _configuration.AccountsAuthenticationRefresh);
} }
public async Task<APIResponse> ChangePassword(ChangePasswordRequest data)
{
return await _apiClient.SendAsync(APIMethodType.PATCH, _configuration.AccountsChangePassword, data);
}
public async Task<APIResponse<IEnumerable<AccountResponse>>> GetAccounts(int? id = null, string? iban = null)
{
Dictionary<string, string> query = new Dictionary<string, string>();
if (id.HasValue)
{
query.Add("id", id.Value.ToString());
}
if (iban is not null)
{
query.Add("iban", iban);
}
return await _apiClient.SendAsync<IEnumerable<AccountResponse>>(APIMethodType.GET, _configuration.AccountsGetAccounts, query);
}
public async Task<APIResponse> ResetPassword(int accountId)
{
string url = string.Format(_configuration.AccountsResetPassword, accountId);
return await _apiClient.SendAsync(APIMethodType.PATCH, url);
}
public async Task<APIResponse> UnlockAccount(int accountId)
{
string url = string.Format(_configuration.AccountsUnlockAccount, accountId);
return await _apiClient.SendAsync(APIMethodType.PATCH, url);
}
#endregion #endregion
} }
} }

View File

@@ -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<APIResponse<decimal>> GetAccountBalance(int accountId);
Task<APIResponse<decimal>> 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<APIResponse<decimal>> GetAccountBalance(int accountId)
{
string url = string.Format(_configuration.BalanceGetAccountBalance, accountId);
return await _apiClient.SendAsync<decimal>(APIMethodType.GET, url);
}
public async Task<APIResponse<decimal>> GetBalance()
{
return await _apiClient.SendAsync<decimal>(APIMethodType.GET, _configuration.BalanceGetBalance);
}
#endregion
}
}

View File

@@ -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<APIResponse> CreateAdminTransfer(CreateAdminTransferRequest data);
Task<APIResponse> CreateUserTransfer(CreateUserTransferRequest data);
Task<APIResponse<IEnumerable<TransferResponse>>> GetTransfers();
Task<APIResponse<IEnumerable<TransferResponse>>> 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<APIResponse<IEnumerable<TransferResponse>>> GetTransfers()
{
return await _apiClient.SendAsync<IEnumerable<TransferResponse>>(APIMethodType.GET, _configuration.TransfersGetTransfers);
}
public async Task<APIResponse<IEnumerable<TransferResponse>>> GetUserTransfers(int accountId)
{
string url = string.Format(_configuration.TransfersGetUserTransfers, accountId);
return await _apiClient.SendAsync<IEnumerable<TransferResponse>>(APIMethodType.GET, url);
}
public async Task<APIResponse> CreateAdminTransfer(CreateAdminTransferRequest data)
{
return await _apiClient.SendAsync(APIMethodType.POST, _configuration.TransfersCreateAdminTransfer, data);
}
public async Task<APIResponse> CreateUserTransfer(CreateUserTransferRequest data)
{
return await _apiClient.SendAsync(APIMethodType.POST, _configuration.TransfersCreateUserTransfer, data);
}
#endregion
}
}

View File

@@ -29,15 +29,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.API.Authenticati
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Website.Authentication", "SecureBank.Website\SecureBank.Website.Authentication\SecureBank.Website.Authentication.csproj", "{4BC964A3-91C9-47FD-9A78-1E43301E9779}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Website.Authentication", "SecureBank.Website\SecureBank.Website.Authentication\SecureBank.Website.Authentication.csproj", "{4BC964A3-91C9-47FD-9A78-1E43301E9779}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A111947-FD57-4EF7-8B03-C46E73B68DE1}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore .dockerignore = .dockerignore
database.db = database.db database.db = database.db
Dockerfile = Dockerfile docker-compose.yml = docker-compose.yml
Main.Dockerfile = Main.Dockerfile
Nginx.Dockerfile = Nginx.Dockerfile
EndProjectSection EndProjectSection
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{4BC964A3-91C9-47FD-9A78-1E43301E9779}.Release|Any CPU.Build.0 = 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 {3A36A494-9B6C-4E18-AE67-064ACD57BAB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A36A494-9B6C-4E18-AE67-064ACD57BAB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A36A494-9B6C-4E18-AE67-064ACD57BAB6}.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}.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -104,6 +118,8 @@ Global
{41FA4292-92B0-4810-8127-6E70E2073D38} = {E23C57D5-1527-4482-963B-374CF1A098D5} {41FA4292-92B0-4810-8127-6E70E2073D38} = {E23C57D5-1527-4482-963B-374CF1A098D5}
{54220F73-24DE-47BB-A027-5326E90A59B6} = {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} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {626ACE64-EFF9-4C9A-84DC-DFF35D76AF59} SolutionGuid = {626ACE64-EFF9-4C9A-84DC-DFF35D76AF59}

View File

@@ -2,12 +2,20 @@
<div class="page"> <div class="page">
<div class="sidebar"> <div class="sidebar">
<NavMenu /> <NavMenu Claims="@_claims"/>
</div> </div>
<main> <main>
<div class="top-row px-4"> <div class="top-row px-4">
<a href="/auth">Login</a> @if (_claims is null)
{
<button class="btn btn-primary" @onclick="@Login">Login</button>
}
else
{
<p class="vertical-center" style="margin: 0px 80px 0px 0px;">Logged as: @($"{_claims.FirstName} {_claims.LastName} ({_claims.AccountId:00000000})")</p>
<button class="btn btn-primary" @onclick="@Logout">Logout</button>
}
</div> </div>
<article class="content px-4"> <article class="content px-4">
@@ -21,3 +29,58 @@
<a href="" class="reload">Reload</a> <a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a> <a class="dismiss">🗙</a>
</div> </div>
@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
}

View File

@@ -12,7 +12,34 @@
<NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink> </NavLink>
@if (Claims is not null)
{
<NavLink class="nav-link" href="/dashboard" Match="NavLinkMatch.All">
<span class="bi bi-dashboard-nav-menu" aria-hidden="true"></span> Dashboard
</NavLink>
<NavLink class="nav-link" href="/create-transfer" Match="NavLinkMatch.All">
<span class="bi bi-transfer-nav-menu" aria-hidden="true"></span> Create transfer
</NavLink>
<NavLink class="nav-link" href="/account-details" Match="NavLinkMatch.All">
<span class="bi bi-account-details-nav-menu" aria-hidden="true"></span> Account details
</NavLink>
@if (Claims.IsAdmin)
{
<NavLink class="nav-link" href="/admin" Match="NavLinkMatch.All">
<span class="bi bi-admin-nav-menu" aria-hidden="true"></span> Admin panel
</NavLink>
}
}
</div> </div>
</nav> </nav>
</div> </div>
@code
{
#region PARAMETERS
[Parameter]
public Claims? Claims { get; set; }
#endregion
}

View File

@@ -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"); 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 { .nav-item {
font-size: 0.9rem; font-size: 0.9rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;

View File

@@ -0,0 +1,185 @@
@page "/admin/accounts-management"
<h3>Accounts</h3>
@if (_authLoaded)
{
@if (_claims is not null && _claims.IsAdmin)
{
@switch (_dataLoadedState)
{
case DataLoadState.Loading:
<p>Waiting for data...</p>
break;
case DataLoadState.NotLoaded:
<p>Data cannot be loaded. Try again later.</p>
<a href="/">Click here to redirect to main page</a>
break;
case DataLoadState.Loaded:
@if (_errorMessage is not null)
{
<p>@_errorMessage</p>
}
<table>
<tr>
<th class="table-brd">Id</th>
<th class="table-brd">Name</th>
<th class="table-brd">Email</th>
<th class="table-brd">Phone number</th>
<th class="table-brd">IBAN</th>
<th class="table-brd">Is password temporary</th>
<th class="table-brd">Is locked</th>
<th class="table-brd">Operations</th>
</tr>
@foreach (var account in _accounts)
{
<tr>
<td class="table-brd">@account.Id</td>
<td class="table-brd">@account.FirstName @account.LastName</td>
<td class="table-brd">@account.Email</td>
<td class="table-brd">@account.PhoneNumber</td>
<td class="table-brd">@account.IBAN</td>
<td class="table-brd">@(account.TemporaryPassword ? "YES" : "NO")</td>
<td class="table-brd">@(account.LockReason is not null || account.LoginFailedCount >= 3 ? "YES" : "NO")</td>
<td class="table-brd">
<button class="btn btn-secondary" @onclick="@(async () => await ResetPassword(account.Id))">Reset password</button>
<button class="btn btn-secondary" @onclick="@(async () => await Unlock(account.Id))" disabled="@(account.LockReason is null && account.LoginFailedCount < 3)">Unlock</button>
</td>
</tr>
}
</table>
break;
}
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
}
else
{
<p>Waiting for authorization...</p>
}
@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<AccountResponse> _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<IEnumerable<AccountResponse>> 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
}

View File

@@ -0,0 +1,337 @@
@page "/admin/create-transfer"
<h3>Create new transfer</h3>
@if (_authLoaded)
{
@if (_claims is not null && _claims.IsAdmin)
{
@switch (_stage)
{
case (Stage.Form):
<EditForm Model="@_data" OnSubmit="SubmitToValidation">
<br />
<p><strong>Sender data:</strong></p>
<div class="form-group">
<label for="sender-account-number-input">
Sender account number:
</label>
<InputText id="sender-account-number-input" class="form-control" minlength="26" maxlength="26" @bind-Value="@_data.SenderAccountNumber"></InputText>
</div>
<div class="form-group">
<label for="sender-name-input">
Sender name:
</label>
<InputText id="sender-name-input" class="form-control" @bind-Value="@_data.SenderName"></InputText>
</div>
<div class="form-group">
<label for="sender-address-input">
Sender address:
</label>
<InputText id="sender-address-input" class="form-control" @bind-Value="@_data.SenderAddress"></InputText>
</div>
<br />
<br />
<p><strong>Receiver data:</strong></p>
<div class="form-group">
<label for="receiver-account-number-input">
Receiver account number:
</label>
<InputText id="receiver-account-number-input" class="form-control" minlength="26" maxlength="26" @bind-Value="@_data.ReceiverAccountNumber"></InputText>
</div>
<div class="form-group">
<label for="receiver-name-input">
Receiver name:
</label>
<InputText id="receiver-name-input" class="form-control" @bind-Value="@_data.ReceiverName"></InputText>
</div>
<div class="form-group">
<label for="receiver-address-input">
Receiver address:
</label>
<InputText id="receiver-address-input" class="form-control" @bind-Value="@_data.ReceiverAddress"></InputText>
</div>
<br />
<br />
<p><strong>Transfer data:</strong></p>
<div class="form-group">
<label for="title-input">
Title:
</label>
<InputText id="title-input" class="form-control" @bind-Value="@_data.Title"></InputText>
</div>
<div class="form-group">
<label for="amount-input">
Amount (PLN):
</label>
<InputNumber id="amount-input" class="form-control" @bind-Value="@_data.Amount"></InputNumber>
</div>
<br />
<br />
<button type="submit" class="btn btn-primary">Submit to validation</button>
</EditForm>
break;
case (Stage.Validated):
<p><strong>Sender data:</strong></p>
<table>
<tr>
<td>Account number:</td>
<td>@_data.SenderAccountNumber</td>
</tr>
<tr>
<td>Name:</td>
<td>@(_data.SenderName ?? "<EMPTY>")</td>
</tr>
<tr>
<td>Address:</td>
<td>@(_data.SenderAddress ?? "<EMPTY>")</td>
</tr>
<tr>
<td>Account balance after transfer:</td>
<td>@_senderAmount</td>
</tr>
</table>
<br />
<br />
<p><strong>Receiver data:</strong></p>
<table>
<tr>
<td>Account number:</td>
<td>@_data.ReceiverAccountNumber</td>
</tr>
<tr>
<td>Name:</td>
<td>@(_data.ReceiverName ?? "<EMPTY>")</td>
</tr>
<tr>
<td>Address:</td>
<td>@(_data.ReceiverAddress ?? "<EMPTY>")</td>
</tr>
<tr>
<td>Account balance after transfer:</td>
<td>@_receiverAmount</td>
</tr>
</table>
<br />
<br />
<p><strong>Transfer data:</strong></p>
<table>
<tr>
<td>Title:</td>
<td>@(_data.Title ?? "<EMPTY>")</td>
</tr>
<tr>
<td>Amount:</td>
<td>@_data.Amount PLN</td>
</tr>
</table>
<br />
<button class="btn btn-primary" type="submit" @onclick="@SubmitTransfer">Submit</button>
break;
case (Stage.Accepted):
<p>Transfer for amount @_data.Amount PLN was successfully sent from account with number @_data.SenderAccountNumber to account with number @_data.ReceiverAccountNumber</p>
<button class="btn btn-secondary" @onclick="@NavigateToAdminPanel">Go back to admin panel</button>
<button class="btn btn-primary" @onclick="@NavigateToNewForm">Create next transfer</button>
break;
}
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<p class="text-red">Error: @_errorMessage</p>
}
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
}
else
{
<p>Waiting for authorization...</p>
}
@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<string> GetBalance(string accountNumber, decimal amount)
{
APIResponse<IEnumerable<AccountResponse>> senderAccountResponse = await _accountsService.GetAccounts(iban: accountNumber);
if (senderAccountResponse.Status == ResponseStatus.Ok && senderAccountResponse.Data.Count() == 1)
{
APIResponse<decimal> senderAmountResponse = await _balanceService.GetAccountBalance(senderAccountResponse.Data.ElementAt(0).Id);
if (senderAmountResponse.Status == ResponseStatus.Ok)
{
return $"{(senderAmountResponse.Data + amount):F2} PLN";
}
else
{
return "<ERROR: Cannot get sender account balance>";
}
}
else
{
return "<NON SECUREBANK ACCOUNT>";
}
}
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
}

View File

@@ -0,0 +1,70 @@
@page "/admin"
<h3>Admin panel</h3>
@if (_authLoaded)
{
@if (_claims is not null && _claims.IsAdmin)
{
<ul>
<li><a href="/admin/create-account">Create new account</a></li>
<li><a href="/admin/create-transfer">Create new transfer</a></li>
<li><a href="/admin/accounts-management">Accounts management</a></li>
</ul>
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
}
else
{
<p>Waiting for authorization...</p>
}
@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
}

View File

@@ -2,8 +2,12 @@
<h3>Create new client account</h3> <h3>Create new client account</h3>
@if (_id is null) @if (_authLoaded || true)
{ {
@if (true || (_claims is not null && _claims.IsAdmin))
{
@if (_id is null)
{
<EditForm Model="@_data" OnSubmit="Submit"> <EditForm Model="@_data" OnSubmit="Submit">
<div class="form-group"> <div class="form-group">
<label for="first-name-input"> <label for="first-name-input">
@@ -29,6 +33,24 @@
</label> </label>
<InputText id="phone-number-input" class="form-control" @bind-Value="@_data.PhoneNumber"></InputText> <InputText id="phone-number-input" class="form-control" @bind-Value="@_data.PhoneNumber"></InputText>
</div> </div>
<div class="form-group">
<label for="address-input">
Address:
</label>
<InputText id="address-input" class="form-control" @bind-Value="@_data.Address"></InputText>
</div>
<div class="form-group">
<label for="pesel-input">
PESEL:
</label>
<InputText id="pesel-input" maxlength="11" minlength="11" class="form-control" @bind-Value="@_data.PESEL"></InputText>
</div>
<div class="form-group">
<label for="id-card-number-input">
Id card number:
</label>
<InputText id="id-card-number-input" class="form-control" maxlength="9" minlength="9" @bind-Value="@_data.IdCardNumber"></InputText>
</div>
<br /> <br />
<button class="btn btn-primary" type="submit">Submit</button> <button class="btn btn-primary" type="submit">Submit</button>
@if (!string.IsNullOrWhiteSpace(_message)) @if (!string.IsNullOrWhiteSpace(_message))
@@ -36,9 +58,9 @@
<p class="text-red">Error: @_message</p> <p class="text-red">Error: @_message</p>
} }
</EditForm> </EditForm>
} }
else else
{ {
<p><strong>New client account was created</strong></p> <p><strong>New client account was created</strong></p>
<table> <table>
<tr> <tr>
@@ -61,16 +83,40 @@ else
<td><strong>Phone number:</strong></td> <td><strong>Phone number:</strong></td>
<td>@_data.PhoneNumber</td> <td>@_data.PhoneNumber</td>
</tr> </tr>
<tr>
<td><strong>Address:</strong></td>
<td>@_data.Address</td>
</tr>
<tr>
<td><strong>PESEL:</strong></td>
<td>@_data.PESEL</td>
</tr>
<tr>
<td><strong>Id card number:</strong></td>
<td>@_data.IdCardNumber</td>
</tr>
<tr> <tr>
<td><strong>Password:</strong></td> <td><strong>Password:</strong></td>
<td>******** (Information passed on to the client - LOG)</td> <td>******** (Information passed on to the client - LOG)</td>
</tr> </tr>
</table> </table>
<br/> <br />
<button class="btn btn-secondary" @onclick="NavigateToAdminPanel">Go back to admin panel</button> <button class="btn btn-secondary" @onclick="NavigateToAdminPanel">Go back to admin panel</button>
<button class="btn btn-primary" @onclick="NavigateToNewForm">Create next account</button> <button class="btn btn-primary" @onclick="NavigateToNewForm">Create next account</button>
}
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
} }
else
{
<p>Waiting for authorization...</p>
}
@code { @code {
#region SERVICES #region SERVICES
@@ -81,6 +127,9 @@ else
[Inject] [Inject]
protected NavigationManager _navigationManager { get; set; } protected NavigationManager _navigationManager { get; set; }
[Inject]
protected TokenAuthenticationStateProvider _authenticationStateProvider { get; set; }
#endregion #endregion
@@ -93,6 +142,10 @@ else
protected string? _message; protected string? _message;
protected Claims? _claims;
protected bool _authLoaded;
#endregion #endregion
@@ -104,12 +157,28 @@ else
_data = new CreateAccountRequest(); _data = new CreateAccountRequest();
_id = null; _id = null;
_message = 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() protected async Task Submit()
{ {
APIResponse<int> response = await _accountsService.CreateAccount(_data); APIResponse<int> response = await _accountsService.CreateAccount(_data);
if (response.Success) if (response.Status == ResponseStatus.Ok)
{ {
_id = response.Data; _id = response.Data;
} }
@@ -121,12 +190,12 @@ else
protected void NavigateToNewForm() protected void NavigateToNewForm()
{ {
OnInitialized(); _navigationManager.Refresh(true);
} }
protected void NavigateToAdminPanel() protected void NavigateToAdminPanel()
{ {
_navigationManager.NavigateTo("/"); //TODO: Zmienić na /admin _navigationManager.NavigateTo("/admin", true);
} }
#endregion #endregion

View File

@@ -2,8 +2,10 @@
<h3>Login</h3> <h3>Login</h3>
@if (!_clientCodeAccepted)
@switch (_state)
{ {
case AuthState.CodeInput:
<p>Enter your client code:</p> <p>Enter your client code:</p>
<form> <form>
@@ -18,15 +20,10 @@
</form> </form>
<br /> <br />
<button type="submit" class="btn btn-primary" @onclick="SubmitClientCode">Next</button> <button type="submit" class="btn btn-primary" @onclick="SubmitClientCode">Next</button>
<br/> <br />
<br/> <br />
@if (!string.IsNullOrWhiteSpace(_clientCodeMessage)) break;
{ case AuthState.PasswordInput:
<p class="text-red">Error: @_clientCodeMessage</p>
}
}
else
{
<p>Enter your password:</p> <p>Enter your password:</p>
<form> <form>
@@ -54,42 +51,93 @@ else
<br /> <br />
<button class="btn" @onclick="OnInitialized">Back</button> <button class="btn" @onclick="OnInitialized">Back</button>
<button type="submit" class="btn btn-primary" @onclick="SubmitClientPassword">Submit</button> <button type="submit" class="btn btn-primary" @onclick="SubmitClientPassword">Submit</button>
@if (!string.IsNullOrWhiteSpace(_clientPasswordMessage)) break;
{ case AuthState.PasswordChange:
<p class="text-red">Error: @_clientPasswordMessage</p> <p>Change temporary password:</p>
} <form>
<div class="form-group">
<label for="password1-input">
Password:
</label>
<InputText id="password1-input" class="form-control" type="password" @bind-Value="_password1" data-index="1" minlength="8" maxlength="20"></InputText>
</div>
<div class="form-group">
<label for="password2-input">
Confirm password:
</label>
<InputText id="password2-input" class="form-control" type="password" @bind-Value="_password2" data-index="1" minlength="8" maxlength="20"></InputText>
</div>
</form>
<br/>
<button class="btn" @onclick="OnInitialized">Back</button>
<button type="submit" class="btn btn-primary" @onclick="SubmitChangePassword">Submit</button>
break;
}
@if (!string.IsNullOrWhiteSpace(_messageError))
{
<p class="text-red">Error: @_messageError</p>
}
@if (!string.IsNullOrWhiteSpace(_messageSuccess))
{
<p class="text-green">@_messageSuccess</p>
} }
@code { @code {
#region ENUMS
protected enum AuthState
{
CodeInput,
PasswordInput,
PasswordChange
}
#endregion
#region SERVICES #region SERVICES
[Inject] [Inject]
protected IAccountsService _accountService { get; set; } protected IAccountsService _accountService { get; set; }
[Inject]
protected AuthenticationHelper _authenticationHelper { get; set; }
[Inject]
protected NavigationManager _navigationManager { get; set; }
[Inject]
protected HttpClient _httpClient { get; set; }
#endregion #endregion
#region FIELDS #region FIELDS
protected bool _clientCodeAccepted; protected AuthState _state;
protected string? _clientCodeMessage; protected string? _messageError;
protected string? _messageSuccess;
protected string[] _clientCodeArr; protected string[] _clientCodeArr;
protected string _clientCode => string.Join(string.Empty, _clientCodeArr); protected string _clientCode => string.Join(string.Empty, _clientCodeArr);
protected string[] _clientPasswordArr; protected string[] _clientPasswordArr;
protected string _clientPassword => string.Join(string.Empty, _clientPasswordArr); protected string _clientPassword => string.Join(string.Empty, _clientPasswordArr);
protected byte _clientPasswordFailCount;
protected string? _clientPasswordMessage;
protected GetPasswordVariantResponse? _loginRequest; protected GetPasswordVariantResponse? _loginRequest;
protected int _accountId;
protected string _password1;
protected string _password2;
#endregion #endregion
@@ -98,53 +146,116 @@ else
protected override void OnInitialized() protected override void OnInitialized()
{ {
_clientCodeAccepted = false; _state = AuthState.CodeInput;
_clientCodeMessage = null; _messageError = null;
_messageSuccess = null;
_clientCodeArr = new string[8]; _clientCodeArr = new string[8];
_clientPasswordArr = new string[20]; _clientPasswordArr = new string[20];
_clientPasswordMessage = null;
_loginRequest = null; _loginRequest = null;
_accountId = 0;
_password1 = string.Empty;
_password2 = string.Empty;
} }
protected async void SubmitClientCode() protected async void SubmitClientCode()
{ {
if (_clientCode.Length == 8 && int.TryParse(_clientCode, out int accountId)) if (_clientCode.Length == 8 && int.TryParse(_clientCode, out _accountId))
{ {
APIResponse<GetPasswordVariantResponse> loginRequest = await _accountService.GetPasswordVariant(accountId); APIResponse<GetPasswordVariantResponse> loginRequest = await _accountService.GetPasswordVariant(_accountId);
if (loginRequest.Success) if (loginRequest.Status == ResponseStatus.Ok)
{ {
if (loginRequest.Data.ValidTo < DateTime.Now) 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 else
{ {
_clientCodeAccepted = true; _state = AuthState.PasswordInput;
_loginRequest = loginRequest.Data; _loginRequest = loginRequest.Data;
_messageError = null;
} }
} }
else else
{ {
_clientCodeMessage = loginRequest.Message; _messageError = loginRequest.Message;
} }
} }
else else
{ {
_clientCodeMessage = "Wrong client code format"; _messageError = "Wrong client code format";
} }
StateHasChanged(); StateHasChanged();
} }
protected void SubmitClientPassword() protected async Task SubmitClientPassword()
{ {
if (_clientPassword.Length == _loginRequest.Indexes.Length) if (_clientPassword.Length == _loginRequest.Indexes.Length)
{ {
AuthenticationRequest requestData = new AuthenticationRequest
{
LoginRequestId = _loginRequest.LoginRequestId,
Password = _clientPassword,
};
APIResponse<string> 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 else
{ {
_clientPasswordMessage = "Not all fields were filled"; 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
{
_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";
} }
} }

View File

@@ -2,6 +2,6 @@
<PageTitle>Home</PageTitle> <PageTitle>Home</PageTitle>
<h1>Hello, world!</h1> <h1>Hello</h1>
Welcome to your new app. Welcome in SecureBank.

View File

@@ -0,0 +1,196 @@
@page "/account-details"
<h3>Account details</h3>
@if (_authLoaded)
{
@if (_claims is not null)
{
@switch (_dataLoadedState)
{
case DataLoadState.Loading:
<p>Waiting for data...</p>
break;
case DataLoadState.NotLoaded:
<p>Data cannot be loaded. Try again later.</p>
<a href="/">Click here to redirect to main page</a>
break;
case DataLoadState.Loaded:
<h4>Personal data:</h4>
<table>
<tr>
<td><strong>First name:</strong></td>
<td>@_data.FirstName</td>
</tr>
<tr>
<td><strong>Last name:</strong></td>
<td>@_data.LastName</td>
</tr>
<tr>
<td><strong>Email:</strong></td>
<td>@_data.Email</td>
</tr>
<tr>
<td><strong>Phone number:</strong></td>
<td>@_data.PhoneNumber</td>
</tr>
<tr>
<td><strong>Address:</strong></td>
<td>@_data.Address</td>
</tr>
<tr>
<td><strong>IBAN:</strong></td>
<td>@_data.IBAN</td>
</tr>
</table>
<br />
<h4>Id card data:</h4>
<table>
<tr>
<td><strong>PESEL:</strong></td>
<td>@(_hideIdCardData ? "***********" : _data.PESEL)</td>
</tr>
<tr>
<td><strong>Id card number:</strong></td>
<td>@(_hideIdCardData ? "*********" : _data.IdCardNumber)</td>
</tr>
</table>
<button class="btn btn-secondary" @onclick="@(() => {_hideIdCardData = !_hideIdCardData; StateHasChanged();})">@(_hideIdCardData ? "Show" : "Hide")</button>
<br />
<br />
<h4>Debit card data:</h4>
<table>
<tr>
<td><strong>Card number:</strong></td>
<td>@(_hideDebitCardData ? "****************" : _data.CardNumber)</td>
</tr>
<tr>
<td><strong>Expiration date:</strong></td>
<td>@(_hideDebitCardData ? "**/**" : _data.CardExpirationDate)</td>
</tr>
<tr>
<td><strong>CVV:</strong></td>
<td>@(_hideDebitCardData ? "***" : _data.CardCVV)</td>
</tr>
</table>
<button class="btn btn-secondary" @onclick="@(() => {_hideDebitCardData = !_hideDebitCardData; StateHasChanged();})">@(_hideDebitCardData ? "Show" : "Hide")</button>
<br/>
<br/>
<h4>Options:</h4>
<button class="btn btn-primary" @onclick="@ChangePassword">Change password</button>
break;
}
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
}
else
{
<p>Waiting for authorization...</p>
}
@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<IEnumerable<AccountResponse>> 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
}

View File

@@ -0,0 +1,140 @@
@page "/account-details/change-password"
<h3>Change password</h3>
@if (_authLoaded)
{
@if (_claims is not null)
{
<form>
<div class="form-group">
<label for="password1-input">
Password:
</label>
<InputText id="password1-input" class="form-control" type="password" @bind-Value="_password1" data-index="1" minlength="8" maxlength="20"></InputText>
</div>
<div class="form-group">
<label for="password2-input">
Confirm password:
</label>
<InputText id="password2-input" class="form-control" type="password" @bind-Value="_password2" data-index="1" minlength="8" maxlength="20"></InputText>
</div>
</form>
<br />
<button type="submit" class="btn btn-primary" @onclick="SubmitChangePassword">Submit</button>
@if (!string.IsNullOrWhiteSpace(_messageError))
{
<p class="text-red">Error: @_messageError</p>
}
@if (!string.IsNullOrWhiteSpace(_messageSuccess))
{
<p class="text-green">@_messageSuccess</p>
}
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
}
else
{
<p>Waiting for authorization...</p>
}
@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
}

View File

@@ -0,0 +1,255 @@
@page "/create-transfer"
<h3>Create new transfer</h3>
@if (_authLoaded)
{
@if (_claims is not null)
{
@switch (_stage)
{
case (Stage.Form):
<EditForm Model="@_data" OnSubmit="SubmitToValidation">
<br />
<p><strong>Receiver data:</strong></p>
<div class="form-group">
<label for="receiver-account-number-input">
Receiver account number:
</label>
<InputText id="receiver-account-number-input" class="form-control" minlength="26" maxlength="26" @bind-Value="@_data.ReceiverAccountNumber"></InputText>
</div>
<div class="form-group">
<label for="receiver-name-input">
Receiver name:
</label>
<InputText id="receiver-name-input" class="form-control" @bind-Value="@_data.ReceiverName"></InputText>
</div>
<div class="form-group">
<label for="receiver-address-input">
Receiver address:
</label>
<InputText id="receiver-address-input" class="form-control" @bind-Value="@_data.ReceiverAddress"></InputText>
</div>
<br />
<br />
<p><strong>Transfer data:</strong></p>
<div class="form-group">
<label for="title-input">
Title:
</label>
<InputText id="title-input" class="form-control" @bind-Value="@_data.Title"></InputText>
</div>
<div class="form-group">
<label for="amount-input">
Amount (PLN):
</label>
<InputNumber id="amount-input" class="form-control" @bind-Value="@_data.Amount"></InputNumber>
</div>
<br />
<br />
<button type="submit" class="btn btn-primary">Submit to validation</button>
</EditForm>
break;
case (Stage.Validated):
<p><strong>Receiver data:</strong></p>
<table>
<tr>
<td>Account number:</td>
<td>@_data.ReceiverAccountNumber</td>
</tr>
<tr>
<td>Name:</td>
<td>@(_data.ReceiverName ?? "<EMPTY>")</td>
</tr>
<tr>
<td>Address:</td>
<td>@(_data.ReceiverAddress ?? "<EMPTY>")</td>
</tr>
</table>
<br />
<br />
<p><strong>Transfer data:</strong></p>
<table>
<tr>
<td>Title:</td>
<td>@(_data.Title ?? "<EMPTY>")</td>
</tr>
<tr>
<td>Amount:</td>
<td>@_data.Amount PLN</td>
</tr>
<tr>
<td>Account balance after transfer:</td>
<td>@_senderAmount</td>
</tr>
</table>
<br />
<button class="btn btn-primary" type="submit" @onclick="@SubmitTransfer">Submit</button>
break;
case (Stage.Accepted):
<p>Transfer for amount @_data.Amount PLN was successfully sent from account to account with number @_data.ReceiverAccountNumber</p>
<button class="btn btn-primary" @onclick="@NavigateToNewForm">Create next transfer</button>
break;
}
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<p class="text-red">Error: @_errorMessage</p>
}
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
}
else
{
<p>Waiting for authorization...</p>
}
@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<decimal> senderAmountResponse = await _balanceService.GetAccountBalance(_claims.AccountId);
if (senderAmountResponse.Status == ResponseStatus.Ok)
{
_senderAmount = $"{(senderAmountResponse.Data - _data.Amount):F2} PLN";
}
else
{
_senderAmount = "<ERROR: Cannot get sender account balance>";
}
_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
}

View File

@@ -0,0 +1,152 @@
@page "/dashboard"
<h3>Dashboard</h3>
@if (_authLoaded)
{
@if (_claims is not null)
{
@switch (_dataLoadedState)
{
case DataLoadState.Loading:
<p>Waiting for data...</p>
break;
case DataLoadState.NotLoaded:
<p>Data cannot be loaded. Try again later.</p>
<a href="/">Click here to redirect to main page</a>
break;
case DataLoadState.Loaded:
<h5>Account balance:</h5>
<p class="text-big">@($"{_balance:F2} PLN")</p>
<h5>Operation history:</h5>
<table class="table-brd">
<tr>
<th class="table-brd">Date</th>
<th class="table-brd">Title</th>
<th class="table-brd">Amount</th>
</tr>
@foreach (TransferResponse operation in _operations.OrderByDescending(x => x.Date))
{
<tr>
<td class="table-brd">@operation.Date.ToString("dd.MM.yyyy HH:mm")</td>
<td class="table-brd">@(string.IsNullOrWhiteSpace(operation.Title) ? "<EMPTY>" : operation.Title)</td>
<td class="table-brd @(_account.IBAN == operation.ReceiverAccountNumber ? "text-green": "text-red")">@($"{(_account.IBAN == operation.SenderAccountNumber ? "- " : string.Empty)}{operation.Amount:F2} PLN")</td>
</tr>
}
</table>
break;
}
}
else
{
<p>You do not have permission to view this page</p>
<a href="/">Click here to redirect to main page</a>
}
}
else
{
<p>Waiting for authorization...</p>
}
@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<TransferResponse> _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<decimal> balanceResponse = await _balanceService.GetBalance();
if (balanceResponse.Status == ResponseStatus.Ok)
{
_balance = balanceResponse.Data;
}
APIResponse<IEnumerable<TransferResponse>> operationsResponse = await _transfersService.GetTransfers();
if (operationsResponse.Status == ResponseStatus.Ok)
{
_operations = operationsResponse.Data;
}
APIResponse<IEnumerable<AccountResponse>> 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
}

View File

@@ -1,13 +1,18 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Net.Http.Headers;
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Authorization
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using SecureBank @using SecureBank
@using SecureBank.Authentication
@using SecureBank.Components @using SecureBank.Components
@using SecureBank.Website.Services; @using SecureBank.Website.Authentication
@using SecureBank.Common; @using SecureBank.Website.Services
@using SecureBank.Common.Accounts; @using SecureBank.Common
@using SecureBank.Common.Accounts
@using SecureBank.Common.Transfers

View File

@@ -1,10 +1,12 @@
using Blazored.SessionStorage; using Blazored.SessionStorage;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using SecureBank.API.Authentication; using SecureBank.API.Authentication;
using SecureBank.API.Encryption;
using SecureBank.Components; using SecureBank.Components;
using SecureBank.Database; using SecureBank.Database;
using SecureBank.Website.API; using SecureBank.Website.API;
@@ -54,8 +56,10 @@ public class Program
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Error"); 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.UseForwardedHeaders(new ForwardedHeadersOptions
app.UseHsts(); {
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
} }
else else
{ {
@@ -68,13 +72,14 @@ public class Program
x.SwaggerEndpoint("/api/swagger/v1/swagger.json", "SecureBank API"); x.SwaggerEndpoint("/api/swagger/v1/swagger.json", "SecureBank API");
x.RoutePrefix = "api/swagger"; x.RoutePrefix = "api/swagger";
}); });
}
app.UseHttpsRedirection(); app.UseHttpsRedirection();
}
app.UseStaticFiles(); app.UseStaticFiles();
app.UseAntiforgery(); app.UseAntiforgery();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
@@ -83,6 +88,17 @@ public class Program
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode(); .AddInteractiveServerRenderMode();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<DatabaseContext>();
if (context.Database.GetPendingMigrations().Any())
{
context.Database.Migrate();
}
}
app.Run(); app.Run();
} }
@@ -136,12 +152,16 @@ public class Program
// Configurations // Configurations
_builder.Services.AddSingleton<AuthenticationConfiguration>(); _builder.Services.AddSingleton<AuthenticationConfiguration>();
_builder.Services.AddSingleton<EncryptionConfiguration>();
// Helpers // Helpers
_builder.Services.AddSingleton<API.Authentication.AuthenticationHelper>(); _builder.Services.AddSingleton<API.Authentication.AuthenticationHelper>();
_builder.Services.AddSingleton<API.Encryption.EncryptionHelper>();
// Services // Services
_builder.Services.AddSingleton<API.Services.IAccountsService, API.Services.AccountsService>(); _builder.Services.AddSingleton<API.Services.IAccountsService, API.Services.AccountsService>();
_builder.Services.AddSingleton<API.Services.IBalanceService, API.Services.BalanceService>();
_builder.Services.AddSingleton<API.Services.ITransfersService, API.Services.TransfersService>();
} }
protected static void BuildWebsite() protected static void BuildWebsite()
@@ -166,6 +186,8 @@ public class Program
// Services // Services
_builder.Services.AddSingleton<Website.Services.IAccountsService, Website.Services.AccountsService>(); _builder.Services.AddSingleton<Website.Services.IAccountsService, Website.Services.AccountsService>();
_builder.Services.AddSingleton<Website.Services.IBalanceService, Website.Services.BalanceService>();
_builder.Services.AddSingleton<Website.Services.ITransfersService, Website.Services.TransfersService>();
} }
#endregion #endregion

View File

@@ -30,8 +30,7 @@
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_HTTPS_PORTS": "443", "ASPNETCORE_HTTP_PORTS": "8080"
"ASPNETCORE_HTTP_PORTS": "80"
}, },
"publishAllPorts": true, "publishAllPorts": true,
"useSSL": true "useSSL": true

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>68c8748d-7175-410d-8bd6-a8ee07e58478</UserSecretsId> <UserSecretsId>68c8748d-7175-410d-8bd6-a8ee07e58478</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileFile>..\Dockerfile</DockerfileFile> <DockerfileFile>..\Main.Dockerfile</DockerfileFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -31,14 +31,22 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SecureBank.API.Encryption\SecureBank.API.Encryption.csproj" />
<ProjectReference Include="..\SecureBank.API\SecureBank.API.Authentication\SecureBank.API.Authentication.csproj" /> <ProjectReference Include="..\SecureBank.API\SecureBank.API.Authentication\SecureBank.API.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.API\SecureBank.API.Controllers\SecureBank.API.Controllers.csproj" /> <ProjectReference Include="..\SecureBank.API\SecureBank.API.Controllers\SecureBank.API.Controllers.csproj" />
<ProjectReference Include="..\SecureBank.API\SecureBank.API.Encryption\SecureBank.API.Encryption.csproj" />
<ProjectReference Include="..\SecureBank.API\SecureBank.API.Services\SecureBank.API.Services.csproj" /> <ProjectReference Include="..\SecureBank.API\SecureBank.API.Services\SecureBank.API.Services.csproj" />
<ProjectReference Include="..\SecureBank.API\SecureBank.API.Token\SecureBank.API.Authentication.csproj" /> <ProjectReference Include="..\SecureBank.API\SecureBank.API.Token\SecureBank.API.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.Authentication\SecureBank.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.Common\SecureBank.Common.csproj" /> <ProjectReference Include="..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\SecureBank.Database\SecureBank.Database.csproj" /> <ProjectReference Include="..\SecureBank.Database\SecureBank.Database.csproj" />
<ProjectReference Include="..\SecureBank.Website\SecureBank.Website.API\SecureBank.Website.API.csproj" />
<ProjectReference Include="..\SecureBank.Website\SecureBank.Website.Authentication\SecureBank.Website.Authentication.csproj" /> <ProjectReference Include="..\SecureBank.Website\SecureBank.Website.Authentication\SecureBank.Website.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.Website\SecureBank.Website.Services\SecureBank.Website.Services.csproj" /> <ProjectReference Include="..\SecureBank.Website\SecureBank.Website.Services\SecureBank.Website.Services.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="..\.dockerignore" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,8 +1,8 @@
{ {
"Logging": { "ConnectionStrings": {
"LogLevel": { "Default": "Data Source = ../database.db"
"Default": "Information", },
"Microsoft.AspNetCore": "Warning" "Endpoints": {
} "Base": "https://localhost:7143/api"
} }
} }

View File

@@ -0,0 +1,8 @@
{
"ConnectionStrings": {
"Default": "Data Source = database.db"
},
"Endpoints": {
"Base": "https://localhost/api"
}
}

View File

@@ -14,17 +14,33 @@
"Lifetime": "5" "Lifetime": "5"
} }
}, },
"ConnectionStrings": { "Encryption": {
"Default": "Data Source = ../database.db" "Key": "NG'MAjEJ'!'bZknqdME^9|nY?x|D$=9*",
"IV": "****************"
}, },
"Endpoints": { "Endpoints": {
"Base": "https://localhost:7143/api",
"Accounts": { "Accounts": {
"Base": "/accounts", "Base": "/accounts",
"CreateAccount": "/create-account", "CreateAccount": "/create-account",
"ChangePassword": "/change-password",
"Authentication": "/authentication",
"AuthenticationRefresh": "/authentication-refresh", "AuthenticationRefresh": "/authentication-refresh",
"GetPasswordVariant": "/{0}/password-variant", "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"
} }
} }
} }

View File

@@ -57,3 +57,25 @@ h1:focus {
.text-red { .text-red {
color: 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;
}

BIN
database-default.db Normal file

Binary file not shown.

21
docker-compose.yml Normal file
View File

@@ -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