This commit is contained in:
2024-01-19 17:25:56 +01:00
Unverified
parent ab9be442ee
commit 5d5a69ccf7
69 changed files with 3769 additions and 0 deletions

30
.dockerignore Normal file
View File

@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

3
.gitignore vendored
View File

@@ -396,3 +396,6 @@ FodyWeavers.xsd
# JetBrains Rider # JetBrains Rider
*.sln.iml *.sln.iml
# SQLite database
*.db

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 443
EXPOSE 80
ENV ASPNETCORE_URLS=https://+:443;http://+:80
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["SecureBank/SecureBank.csproj", "SecureBank/"]
COPY ["SecureBank.API/SecureBank.API.Authentication/SecureBank.API.Authentication.csproj", "SecureBank.API/SecureBank.API.Authentication/"]
COPY ["SecureBank.Database/SecureBank.Database.csproj", "SecureBank.Database/"]
COPY ["SecureBank.API/SecureBank.API.Controllers/SecureBank.API.Controllers.csproj", "SecureBank.API/SecureBank.API.Controllers/"]
COPY ["SecureBank.Common/SecureBank.Common.csproj", "SecureBank.Common/"]
COPY ["SecureBank.API/SecureBank.API.Services/SecureBank.API.Services.csproj", "SecureBank.API/SecureBank.API.Services/"]
COPY ["SecureBank.Extensions/SecureBank.Extensions.csproj", "SecureBank.Extensions/"]
COPY ["SecureBank.API/SecureBank.API.Helpers/SecureBank.API.Helpers.csproj", "SecureBank.API/SecureBank.API.Helpers/"]
COPY ["SecureBank.Website/SecureBank.Website.Authentication/SecureBank.Website.Authentication.csproj", "SecureBank.Website/SecureBank.Website.Authentication/"]
COPY ["SecureBank.Helpers/SecureBank.Helpers.csproj", "SecureBank.Helpers/"]
COPY ["SecureBank.Website/SecureBank.Website.Services/SecureBank.Website.Services.csproj", "SecureBank.Website/SecureBank.Website.Services/"]
COPY ["SecureBank.Website/SecureBank.Website.API/SecureBank.Website.API.csproj", "SecureBank.Website/SecureBank.Website.API/"]
RUN dotnet restore "./SecureBank/./SecureBank.csproj"
COPY . .
WORKDIR "/src/SecureBank"
RUN dotnet build "./SecureBank.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./SecureBank.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "SecureBank.dll"]

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Authentication
{
public class AuthenticationConfiguration
{
#region PROPERTIES
// Token
public string TokenKey { get; private set; }
public string TokenIssuer { get; private set; }
public string TokenAudience { get; private set; }
public int TokenLifetime { get; private set; }
#endregion
#region CONSTRUCTORS
public AuthenticationConfiguration(IConfiguration configuration)
{
TokenKey = configuration.GetSection("Authentication").GetSection("Token")["Key"];
TokenIssuer = configuration.GetSection("Authentication").GetSection("Token")["Issuer"];
TokenAudience = configuration.GetSection("Authentication").GetSection("Token")["Audience"];
TokenLifetime = int.Parse(configuration.GetSection("Authentication").GetSection("Token")["Lifetime"]);
}
#endregion
}
}

View File

@@ -0,0 +1,69 @@
using Microsoft.IdentityModel.Tokens;
using SecureBank.Database;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Permissions;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Authentication
{
public class AuthenticationHelper
{
#region SERVICES
private DatabaseContext _database;
private AuthenticationConfiguration _configuration;
#endregion
#region CONSTRUCTORS
public AuthenticationHelper(DatabaseContext database, AuthenticationConfiguration configuration)
{
_database = database;
_configuration = configuration;
}
#endregion
#region METHODS
public string GenerateToken(Guid tokenId, int accountId, bool oneTimeToken = false)
{
DateTime expirationTime = DateTime.UtcNow.AddMinutes(_configuration.TokenLifetime);
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, tokenId.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, accountId.ToString()),
new Claim(JwtRegisteredClaimNames.Exp, expirationTime.ToString()),
new Claim("one_time_token", oneTimeToken.ToString()),
new Claim("admin", "false"), //TODO: w zależności od użytkownika
}),
Expires = expirationTime,
Issuer = _configuration.TokenIssuer,
Audience = _configuration.TokenAudience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.TokenKey)), SecurityAlgorithms.HmacSha512)
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
handler.InboundClaimTypeMap.Clear();
SecurityToken token = handler.CreateToken(tokenDescriptor);
return handler.WriteToken(token);
}
#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>
<ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Client;
using SecureBank.API.Services;
using SecureBank.Common;
using SecureBank.Common.Accounts;
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/accounts")]
public class AccountsController : ControllerBase
{
#region SERVICES
private IAccountsService _accountsService;
#endregion
#region CONSTRUCTORS
public AccountsController(IAccountsService accountsService)
{
_accountsService = accountsService;
}
#endregion
#region METHODS
[HttpPost]
[Route("create-account")]
[AllowAnonymous]
public async Task<ActionResult<APIResponse<int>>> CreateAccount([FromBody] CreateAccountRequest data)
{
APIResponse<int> response = await _accountsService.CreateAccount(data);
if (response.Success)
{
return Ok(response);
}
else
{
return BadRequest(response);
}
}
[HttpGet]
[Route("{account_id}/password-variant")]
[AllowAnonymous]
public async Task<ActionResult<APIResponse<GetPasswordVariantResponse>>> GetPasswordVariant([FromRoute(Name = "account_id")] int accountId)
{
APIResponse<GetPasswordVariantResponse> response = await _accountsService.GetPasswordVariant(accountId);
if (response.Success)
{
return Ok(response);
}
else
{
return BadRequest(response);
}
}
[HttpPost]
[Route("{account_id}/authentication")]
[AllowAnonymous]
/*
* Action codes:
* 1 - Go back to client code input
* 2 - Failed login count increment
*/
public async Task<ActionResult<APIResponse<string>>> Authentication([FromRoute(Name = "account_id")] int accountId, [FromBody] AuthenticationRequest data)
{
APIResponse<string> response = await _accountsService.Authentication(accountId, data);
if (response.Success)
{
return Ok(response);
}
else
{
return BadRequest(response);
}
}
#endregion
}
}

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"SecureBank.API": {
"commandName": "Project"
},
"Docker": {
"commandName": "Docker"
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\SecureBank.API.Services\SecureBank.API.Services.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Helpers
{
public class Check<T>
{
public Predicate<T> CheckAction { get; set; }
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,383 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SecureBank.API.Helpers;
using SecureBank.API.Authentication;
using SecureBank.Common;
using SecureBank.Common.Accounts;
using SecureBank.Database;
using SecureBank.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.Arm;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Services
{
public interface IAccountsService
{
Task<APIResponse<int>> CreateAccount(CreateAccountRequest data);
Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId);
Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data);
}
public class AccountsService : IAccountsService
{
#region SERVICES
private AuthenticationHelper _authenticationHelper;
private DatabaseContext _database;
private ILogger<AccountsService> _logger;
#endregion
#region CONSTRUCTORS
public AccountsService(AuthenticationHelper authenticationHelper, DatabaseContext database, ILogger<AccountsService> logger)
{
_authenticationHelper = authenticationHelper;
_database = database;
_logger = logger;
}
#endregion
#region PUBLIC METHODS
public async Task<APIResponse<int>> CreateAccount(CreateAccountRequest data)
{
Check<CreateAccountRequest>[] checks = new Check<CreateAccountRequest>[]
{
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => x is null),
Message = "Body cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.FirstName)),
Message = "First name cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.LastName)),
Message = "Last name cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.Email)),
Message = "Email cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) =>
{
try
{
MailAddress m = new MailAddress(x.Email);
}
catch (FormatException ex)
{
return true;
}
return false;
}),
Message = "Invalid email"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.PhoneNumber)),
Message = "Phone number cannot be empty"
},
};
foreach (Check<CreateAccountRequest> check in checks)
{
if (check.CheckAction.Invoke(data))
{
return new APIResponse<int>
{
Message = check.Message,
Success = false
};
}
}
Account account = new Account
{
FirstName = data.FirstName,
LastName = data.LastName,
Email = data.Email,
PhoneNumber = data.PhoneNumber.Replace(" ", string.Empty),
};
await _database.Accounts.AddAsync(account);
await _database.SaveChangesAsync();
string password = GeneratePassword();
await GeneratePasswordVariants(password, account.Id);
//Send client code and temporary password to client by mail
_logger.LogInformation($"INFO DIRECTLY TO CLIENT: Your client code is {account.Id:00000000}. Your temporary password is {password}. You will be prompted to change it at first login");
return new APIResponse<int>
{
Data = account.Id,
Success = true
};
}
public async Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<GetPasswordVariantResponse>
{
Success = false,
Message = $"Account does not exists"
};
}
if (account.LoginFailedCount >= 3)
{
return new APIResponse<GetPasswordVariantResponse>
{
Success = false,
Message = $"The number of failed login attempts for this account has exceeded 3. Contact your bank to confirm your identity and unlock your account."
};
}
if (account.LockReason is not null)
{
return new APIResponse<GetPasswordVariantResponse>
{
Success = false,
Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account."
};
}
IEnumerable<AccountPassword> accountPasswords = await _database.AccountPasswords.Where(x => x.AccountId == accountId).ToArrayAsync();
int randomIndex = Random.Shared.Next(0, accountPasswords.Count());
AccountPassword passwordVariant = accountPasswords.ElementAt(randomIndex);
AccountPasswordIndex[] indexes = await _database.AccountPasswordIndexes.Where(x => x.AccountPasswordId == passwordVariant.Id).ToArrayAsync();
DateTime validTo = DateTime.Now.AddMinutes(5);
AccountLoginRequest loginRequest = new AccountLoginRequest
{
AccountPasswordId = passwordVariant.Id,
ValidTo = validTo,
};
await _database.AccountLoginRequests.AddAsync(loginRequest);
await _database.SaveChangesAsync();
return new APIResponse<GetPasswordVariantResponse>
{
Success = true,
Data = new GetPasswordVariantResponse
{
LoginRequestId = loginRequest.Id,
Indexes = indexes.Select(x => (int)x.Index).ToArray(),
ValidTo = validTo
}
};
}
public async Task<APIResponse<string>> Authentication(int accountId, 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);
if (loginRequest is null)
{
return new APIResponse<string>
{
Success = false,
Message = $"Login request does not exist"
};
}
AccountPassword password = loginRequest.AccountPassword;
Account loginRequestAccount = password.Account;
if (loginRequestAccount.Id != account.Id)
{
account.LockReason = "Suspicious login attempt. The account provided does not match the account to which the login request is assigned.";
loginRequestAccount.LockReason = "Suspicious login attempt. The account provided does not match the account to which the login request is assigned.";
await _database.SaveChangesAsync();
return new APIResponse<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)
{
return new APIResponse<string>
{
Success = false,
ActionCode = 1,
Message = $"Login request has expired. Go back and try again."
};
}
byte[] passwordDb = password.Password;
byte[] passwordProvided = HashPassword(data.Password, password.LeftSalt, password.RightSalt);
if (Enumerable.SequenceEqual(passwordDb, passwordProvided))
{
account.LoginFailedCount++;
await _database.SaveChangesAsync();
return new APIResponse<string>
{
Success = false,
ActionCode = 2,
Message = $"Incorrect password"
};
}
string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), account.Id, account.TemporaryPassword);
return new APIResponse<string>
{
Data = token,
Success = true,
};
}
#endregion
#region PRIVATE METHODS
protected byte[] HashPassword(string password, string leftSalt, string rightSalt)
{
SHA512 sha = SHA512.Create();
string toHash = password;
for (int c = 0; c < 100; c++)
{
string before = $"{leftSalt}{toHash}{rightSalt}";
byte[] bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(before));
toHash = Encoding.UTF8.GetString(bytes);
}
return Encoding.UTF8.GetBytes(toHash);
}
protected async Task GeneratePasswordVariants(string password, int accountId)
{
int charCount = password.Length / 2;
IEnumerable<int> charIndexes = Enumerable.Range(0, password.Length);
IEnumerable<IEnumerable<int>> indexesVariants = charIndexes.GetCombinations(charCount).OrderBy(x => Random.Shared.Next()).Take(50);
foreach (IEnumerable<int> indexes in indexesVariants)
{
List<char> chars = new List<char>();
foreach (int i in indexes)
{
chars.Add(password[i]);
}
string leftSalt = StringExtensions.CreateRandom(20);
string rightSalt = StringExtensions.CreateRandom(20);
string toHash = string.Join(string.Empty, chars);
byte[] hashed = HashPassword(toHash, leftSalt, rightSalt);
AccountPassword accountPassword = new AccountPassword
{
AccountId = accountId,
LeftSalt = leftSalt,
RightSalt = rightSalt,
Password = hashed,
};
await _database.AccountPasswords.AddAsync(accountPassword);
await _database.SaveChangesAsync();
IEnumerable<AccountPasswordIndex> indexesDB = indexes.Select(x => new AccountPasswordIndex
{
AccountPasswordId = accountPassword.Id,
Index = (byte)x
});
await _database.AccountPasswordIndexes.AddRangeAsync(indexesDB);
await _database.SaveChangesAsync();
}
}
protected string GeneratePassword()
{
string passwordDigits = StringExtensions.CreateRandom(2, "1234567890");
string passwordSymbols = StringExtensions.CreateRandom(2, "`~!@#$%^&*()-_=+[{]};:'\"\\|,<.>/?");
string passwordSmall = StringExtensions.CreateRandom(2, "qwertyuiopasdfghjklzxcvbnm");
string passwordBig = StringExtensions.CreateRandom(2, "QWERTYUIOPASDFGHJKLZXCVBNM");
return string.Concat(passwordDigits, passwordSymbols, passwordSmall, passwordBig).Shuffle();
}
protected IEnumerable<string> CheckPassword(string password)
{
int minLength = 8;
if (password.Length < minLength)
{
yield return $"Password must be at least {minLength} characters long";
}
if (!password.Any(x => Char.IsUpper(x)))
{
yield return $"Password must contain at least one uppercase character";
}
if (!password.Any(x => Char.IsLower(x)))
{
yield return $"Password must contain at least one lowercase character";
}
if (!password.Any(x => Char.IsDigit(x)))
{
yield return $"Password must contain at least one digit";
}
if (!password.Any(x => Char.IsSymbol(x)))
{
yield return $"Password must contain at least one special character";
}
}
#endregion
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" />
<ProjectReference Include="..\..\SecureBank.Extensions\SecureBank.Extensions.csproj" />
<ProjectReference Include="..\SecureBank.API.Authentication\SecureBank.API.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.API.Helpers\SecureBank.API.Helpers.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
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
{
public class APIResponse
{
[JsonProperty("message")]
[JsonPropertyName("message")]
public string Message { get; set; }
[JsonProperty("success")]
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonProperty("action_code")]
[JsonPropertyName("action_code")]
public int ActionCode { get; set; }
}
public class APIResponse<T> : APIResponse
{
[JsonProperty("data")]
[JsonPropertyName("data")]
public T Data { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
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 AuthenticationRequest
{
[JsonProperty("login_request_id")]
[JsonPropertyName("login_request_id")]
public Guid LoginRequestId { get; set; }
[JsonProperty("password")]
[JsonPropertyName("password")]
public string Password { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
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 CreateAccountRequest
{
[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; }
}
}

View File

@@ -0,0 +1,25 @@
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 GetPasswordVariantResponse
{
[JsonProperty("login_request_id")]
[JsonPropertyName("login_request_id")]
public Guid LoginRequestId { get; set; }
[JsonProperty("indexes")]
[JsonPropertyName("indexes")]
public int[] Indexes { get; set; }
[JsonProperty("valid_to")]
[JsonPropertyName("valid_to")]
public DateTime ValidTo { get; set; }
}
}

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="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Database
{
public partial class Account
{
#region PROPERTIES
[Key]
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string FirstName { get; set; }
[Required]
[MaxLength(100)]
public string LastName { get; set; }
[Required]
[MaxLength(300)]
public string Email { get; set; }
[Required]
[MaxLength(20)]
public string PhoneNumber { get; set; }
[Required]
public byte LoginFailedCount { get; set; } = 0;
[Required]
public bool TemporaryPassword { get; set; } = true;
[MaxLength(1000)]
public string? LockReason { get; set; } = null;
#endregion
#region NAVIGATION
public virtual ICollection<AccountPassword> AccountPasswords { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,35 @@
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 class AccountLoginRequest
{
#region PROPERTIES
[Key]
public Guid Id { get; set; }
[Required]
[ForeignKey(nameof(AccountPassword))]
public long AccountPasswordId { get; set; }
[Required]
public DateTime ValidTo { get; set; }
#endregion
#region NAVIGATION
public AccountPassword AccountPassword { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,44 @@
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 AccountPassword
{
#region PROPERTIES
[Key]
public long Id { get; set; }
[Required]
[ForeignKey(nameof(Account))]
public int AccountId { get; set; }
[Required]
[MaxLength(1000)]
public byte[] Password { get; set; }
[Required]
[MaxLength(20)]
public string LeftSalt { get; set; }
[Required]
[MaxLength(20)]
public string RightSalt { get; set; }
#endregion
#region NAVIGATION
public Account Account { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,36 @@
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 class AccountPasswordIndex
{
#region PROPERTIES
[Key]
public long Id { get; set; }
[Required]
[ForeignKey(nameof(AccountPassword))]
public long AccountPasswordId { get; set; }
[Required]
public byte Index { get; set; }
#endregion
#region NAVIGATION
public AccountPassword AccountPassword { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Database
{
public class DatabaseContext : DbContext
{
#region CONSTRUCTORS
public DatabaseContext() { }
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { }
#endregion
#region PROPERTIES
public virtual DbSet<Account> Accounts { get; set; }
public virtual DbSet<AccountPassword> AccountPasswords { get; set; }
public virtual DbSet<AccountPasswordIndex> AccountPasswordIndexes { get; set; }
public virtual DbSet<AccountLoginRequest> AccountLoginRequests { get; set; }
#endregion
#region METHODS
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlite("name=Default");
#endregion
}
}

View File

@@ -0,0 +1,132 @@
// <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

@@ -0,0 +1,94 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SecureBank.Database.Migrations
{
/// <inheritdoc />
public partial class Migration1 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Accounts",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
FirstName = 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),
PhoneNumber = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
LoginFailedCount = table.Column<byte>(type: "INTEGER", nullable: false),
TemporaryPassword = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Accounts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AccountPasswords",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AccountId = table.Column<int>(type: "INTEGER", nullable: false),
Password = table.Column<byte[]>(type: "BLOB", maxLength: 1000, nullable: false),
LeftSalt = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
RightSalt = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AccountPasswords", x => x.Id);
table.ForeignKey(
name: "FK_AccountPasswords_Accounts_AccountId",
column: x => x.AccountId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AccountPasswordIndexes",
columns: table => new
{
AccountPasswordId = table.Column<long>(type: "INTEGER", nullable: false),
Index = table.Column<byte>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.ForeignKey(
name: "FK_AccountPasswordIndexes_AccountPasswords_AccountPasswordId",
column: x => x.AccountPasswordId,
principalTable: "AccountPasswords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AccountPasswordIndexes_AccountPasswordId",
table: "AccountPasswordIndexes",
column: "AccountPasswordId");
migrationBuilder.CreateIndex(
name: "IX_AccountPasswords_AccountId",
table: "AccountPasswords",
column: "AccountId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AccountPasswordIndexes");
migrationBuilder.DropTable(
name: "AccountPasswords");
migrationBuilder.DropTable(
name: "Accounts");
}
}
}

View File

@@ -0,0 +1,138 @@
// <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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,169 @@
// <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

@@ -0,0 +1,46 @@
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

@@ -0,0 +1,173 @@
// <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("20240115132220_Migration4")]
partial class Migration4
{
/// <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<string>("LockReason")
.HasMaxLength(1000)
.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

@@ -0,0 +1,29 @@
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

@@ -0,0 +1,170 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SecureBank.Database;
#nullable disable
namespace SecureBank.Database.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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<string>("LockReason")
.HasMaxLength(1000)
.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

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,78 @@
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.Extensions
{
public static class IEnumerableExtensions
{
#region PUBLIC METHODS
public static IEnumerable<IEnumerable<T>> GetCombinations<T>(this IEnumerable<T> elements, int n)
{
if (elements.Count() < n)
{
throw new ArgumentException("Array length can't be less than number of selected elements");
}
if (n < 1)
{
throw new ArgumentException("Number of selected elements can't be less than 1");
}
if (elements.Count() == n)
{
return new List<IEnumerable<T>> { elements.ToList() };
}
List<List<T>> result = new List<List<T>>();
int[] indices = Enumerable.Range(0, n).ToArray();
while (indices[0] <= elements.Count() - n)
{
List<T> combination = new List<T>();
for (int i = 0; i < n; i++)
{
combination.Add(elements.ElementAt(indices[i]));
}
result.Add(combination);
int j = n - 1;
while (j >= 0 && indices[j] == elements.Count() - n + j)
{
j--;
}
if (j < 0)
{
break;
}
indices[j]++;
for (int k = j + 1; k < n; k++)
{
indices[k] = indices[k - 1] + 1;
}
}
return result;
}
#endregion
#region PRIVATE METHODS
#endregion
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Extensions
{
public static class IListExtensions
{
public static void Shuffle<T>(this IList<T> list)
{
Random rng = Random.Shared;
int n = list.Count;
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Extensions
{
public static class StringExtensions
{
public static string CreateRandom(int length) => CreateRandom(length, "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890`~!@#$%^&*()-_=+[{]};:'\"\\|,<.>/?");
public static string CreateRandom(int length, IEnumerable<char> characters) => new string(Enumerable.Repeat(characters, length).Select(s => s.ElementAt(Random.Shared.Next(s.Count()))).ToArray());
public static string Shuffle(this string str)
{
char[] array = str.ToCharArray();
Random rng = Random.Shared;
int n = array.Length;
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
char value = array[k];
array[k] = array[n];
array[n] = value;
}
return new string(array);
}
}
}

View File

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,107 @@
using Newtonsoft.Json;
using SecureBank.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Website.API
{
public class APIClient
{
#region FIELDS
private readonly HttpClient _httpClient;
#endregion
#region CONSTRUCTORS
public APIClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
#endregion
#region PUBLIC METHODS
public async Task<APIResponse<TResponse>> SendAsync<TResponse>(APIMethodType type, string url)
{
return await SendRequestAsync<APIResponse<TResponse>>(type, url, null);
}
public async Task<APIResponse> SendAsync(APIMethodType type, string url)
{
return await SendRequestAsync<APIResponse>(type, url, null);
}
public async Task<APIResponse<TResponse>> SendAsync<TResponse, TBody>(APIMethodType type, string url, TBody body)
{
HttpContent content = PrepareBody(body);
return await SendRequestAsync<APIResponse<TResponse>>(type, url, content);
}
public async Task<APIResponse> SendAsync<TBody>(APIMethodType type, string url, TBody body)
{
HttpContent content = PrepareBody(body);
return await SendRequestAsync<APIResponse>(type, url, content);
}
#endregion
#region PRIVATE METHODS
private HttpContent PrepareBody<T>(T body)
{
string json = JsonConvert.SerializeObject(body);
HttpContent content = new StringContent(json);
content.Headers.ContentType.MediaType = "application/json";
return content;
}
private async Task<T> SendRequestAsync<T>(APIMethodType type, string url, HttpContent? content)
{
try
{
HttpResponseMessage response = type switch
{
APIMethodType.GET => await _httpClient.GetAsync(url),
APIMethodType.POST => await _httpClient.PostAsync(url, content),
APIMethodType.PUT => await _httpClient.PutAsync(url, content),
APIMethodType.DELETE => await _httpClient.DeleteAsync(url),
_ => throw new NotImplementedException()
};
string responseBodyString = await response.Content.ReadAsStringAsync();
T? responseBodyObject = JsonConvert.DeserializeObject<T>(responseBodyString);
if (responseBodyObject is null)
{
throw new Exception($"Wrong response type. Response: {responseBodyString}; {response.StatusCode}");
}
return responseBodyObject;
}
catch (Exception ex)
{
Console.WriteLine(ex);
throw;
}
}
#endregion
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Website.API
{
public class APIEndpointsConfiguration
{
#region PROPERTIES
public string Base { get; private set; }
// Accounts
public string AccountsBase { get; private set; }
public string AccountsCreateAccount { get; private set; }
public string AccountsGetPasswordVariant { get; private set; }
public string AccountsAuthentication { get; private set; }
public string AccountsAuthenticationRefresh { get; private set; }
#endregion
#region CONSTRUCTORS
public APIEndpointsConfiguration(IConfiguration configuration)
{
Base = configuration.GetSection("Endpoints")["Base"];
AccountsBase = $"{Base}{configuration.GetSection("Endpoints").GetSection("Accounts")["Base"]}";
AccountsCreateAccount = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["CreateAccount"]}";
AccountsGetPasswordVariant = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["GetPasswordVariant"]}";
AccountsAuthentication = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["Authentication"]}";
AccountsAuthenticationRefresh = $"{AccountsBase}{configuration.GetSection("Endpoints").GetSection("Accounts")["AuthenticationRefresh"]}";
}
#endregion
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Website.API
{
public enum APIMethodType
{
GET,
POST,
PUT,
DELETE
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,49 @@
using Blazored.SessionStorage;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Website.Authentication
{
public class AuthenticationHelper
{
#region CONSTANTS
private const string TOKEN_KEY = "token";
#endregion
#region SERVICES
private readonly ISessionStorageService _sessionStorageService;
#endregion
#region CONSTRUCTIONS
public AuthenticationHelper(ISessionStorageService sessionStorageService)
{
_sessionStorageService = sessionStorageService;
}
#endregion
#region PUBLIC METHODS
public async Task<string> GetToken() => await _sessionStorageService.GetItemAsync<string>(TOKEN_KEY);
public async Task SaveToken(string token) => await _sessionStorageService.SetItemAsync(TOKEN_KEY, token);
public async Task RemoveToken() => await _sessionStorageService.RemoveItemAsync(TOKEN_KEY);
#endregion
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Helpers\SecureBank.Helpers.csproj" />
<ProjectReference Include="..\SecureBank.Website.Services\SecureBank.Website.Services.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Components.Authorization;
using SecureBank.Common;
using SecureBank.Website.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Website.Authentication
{
public class TokenAuthenticationStateProvider : AuthenticationStateProvider
{
#region SERVICES
private readonly IAccountsService _accountsService;
private readonly AuthenticationHelper _authenticationHelper;
private readonly HttpClient _httpClient;
#endregion
#region CONSTRUCTORS
public TokenAuthenticationStateProvider(IAccountsService accountsService, AuthenticationHelper authenticationHelper, HttpClient httpClient)
{
_accountsService = accountsService;
_authenticationHelper = authenticationHelper;
_httpClient = httpClient;
}
#endregion
#region PUBLIC METHODS
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
_httpClient.DefaultRequestHeaders.Authorization = null;
AuthenticationState state = new AuthenticationState(new ClaimsPrincipal());
string token = await _authenticationHelper.GetToken();
if (string.IsNullOrWhiteSpace(token))
{
return state;
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
APIResponse<string> refreshResponse = await _accountsService.AuthenticationRefresh();
if (!refreshResponse.Success)
{
_httpClient.DefaultRequestHeaders.Authorization = null;
return state;
}
token = refreshResponse.Data;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
state = new AuthenticationState(new ClaimsPrincipal()); //TODO: Add claims
return state;
}
#endregion
}
}

View File

@@ -0,0 +1,71 @@
using SecureBank.Common;
using SecureBank.Common.Accounts;
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 IAccountsService
{
Task<APIResponse<int>> CreateAccount(CreateAccountRequest data);
Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId);
Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data);
Task<APIResponse<string>> AuthenticationRefresh();
}
public class AccountsService : IAccountsService
{
#region FIELDS
private readonly APIClient _apiClient;
private readonly APIEndpointsConfiguration _configuration;
#endregion
#region CONSTRUCTORS
public AccountsService(APIClient apiClient, APIEndpointsConfiguration configuration)
{
_apiClient = apiClient;
_configuration = configuration;
}
#endregion
#region METHODS
public async Task<APIResponse<int>> CreateAccount(CreateAccountRequest data)
{
return await _apiClient.SendAsync<int, CreateAccountRequest>(APIMethodType.POST, _configuration.AccountsCreateAccount, data);
}
public async Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId)
{
string url = string.Format(_configuration.AccountsGetPasswordVariant, accountId);
return await _apiClient.SendAsync<GetPasswordVariantResponse>(APIMethodType.GET, url);
}
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, url, data);
}
public async Task<APIResponse<string>> AuthenticationRefresh()
{
return await _apiClient.SendAsync<string>(APIMethodType.POST, _configuration.AccountsAuthenticationRefresh);
}
#endregion
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\SecureBank.Website.API\SecureBank.Website.API.csproj" />
</ItemGroup>
</Project>

111
SecureBank.sln Normal file
View File

@@ -0,0 +1,111 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Database", "SecureBank.Database\SecureBank.Database.csproj", "{A4D5C9AE-AD8E-47AE-B4F2-99D8CBC288F0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Common", "SecureBank.Common\SecureBank.Common.csproj", "{47381470-8ABF-483F-B56C-8666A99ABD54}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank", "SecureBank\SecureBank.csproj", "{EBF41963-F62F-40A8-8B6F-5577C6B68BCA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Extensions", "SecureBank.Extensions\SecureBank.Extensions.csproj", "{5754E04B-0AA0-427B-B30D-5D4BC0EFFB54}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SecureBank.API", "SecureBank.API", "{E23C57D5-1527-4482-963B-374CF1A098D5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SecureBank.Website", "SecureBank.Website", "{EFBD13EE-AF89-4792-A3DE-FF38BDA38DD0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.API.Controllers", "SecureBank.API\SecureBank.API.Controllers\SecureBank.API.Controllers.csproj", "{BA4E2E64-12B4-4AA3-903D-10251D940662}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.API.Services", "SecureBank.API\SecureBank.API.Services\SecureBank.API.Services.csproj", "{7D19503F-122B-4FDF-89DF-475DF0ACAAD7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Website.Services", "SecureBank.Website\SecureBank.Website.Services\SecureBank.Website.Services.csproj", "{CF349ABB-598D-4F46-A10C-09AE7F697977}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Website.API", "SecureBank.Website\SecureBank.Website.API\SecureBank.Website.API.csproj", "{AABF8712-9B04-4EC9-B221-423352DA2993}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.API.Helpers", "SecureBank.API\SecureBank.API.Helpers\SecureBank.API.Helpers.csproj", "{41FA4292-92B0-4810-8127-6E70E2073D38}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.API.Authentication", "SecureBank.API\SecureBank.API.Authentication\SecureBank.API.Authentication.csproj", "{54220F73-24DE-47BB-A027-5326E90A59B6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Website.Authentication", "SecureBank.Website\SecureBank.Website.Authentication\SecureBank.Website.Authentication.csproj", "{4BC964A3-91C9-47FD-9A78-1E43301E9779}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureBank.Helpers", "SecureBank.Helpers\SecureBank.Helpers.csproj", "{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A111947-FD57-4EF7-8B03-C46E73B68DE1}"
ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
database.db = database.db
Dockerfile = Dockerfile
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A4D5C9AE-AD8E-47AE-B4F2-99D8CBC288F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4D5C9AE-AD8E-47AE-B4F2-99D8CBC288F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4D5C9AE-AD8E-47AE-B4F2-99D8CBC288F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4D5C9AE-AD8E-47AE-B4F2-99D8CBC288F0}.Release|Any CPU.Build.0 = Release|Any CPU
{47381470-8ABF-483F-B56C-8666A99ABD54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47381470-8ABF-483F-B56C-8666A99ABD54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47381470-8ABF-483F-B56C-8666A99ABD54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47381470-8ABF-483F-B56C-8666A99ABD54}.Release|Any CPU.Build.0 = Release|Any CPU
{EBF41963-F62F-40A8-8B6F-5577C6B68BCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBF41963-F62F-40A8-8B6F-5577C6B68BCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBF41963-F62F-40A8-8B6F-5577C6B68BCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBF41963-F62F-40A8-8B6F-5577C6B68BCA}.Release|Any CPU.Build.0 = Release|Any CPU
{5754E04B-0AA0-427B-B30D-5D4BC0EFFB54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5754E04B-0AA0-427B-B30D-5D4BC0EFFB54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5754E04B-0AA0-427B-B30D-5D4BC0EFFB54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5754E04B-0AA0-427B-B30D-5D4BC0EFFB54}.Release|Any CPU.Build.0 = Release|Any CPU
{BA4E2E64-12B4-4AA3-903D-10251D940662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA4E2E64-12B4-4AA3-903D-10251D940662}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA4E2E64-12B4-4AA3-903D-10251D940662}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA4E2E64-12B4-4AA3-903D-10251D940662}.Release|Any CPU.Build.0 = Release|Any CPU
{7D19503F-122B-4FDF-89DF-475DF0ACAAD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D19503F-122B-4FDF-89DF-475DF0ACAAD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D19503F-122B-4FDF-89DF-475DF0ACAAD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D19503F-122B-4FDF-89DF-475DF0ACAAD7}.Release|Any CPU.Build.0 = Release|Any CPU
{CF349ABB-598D-4F46-A10C-09AE7F697977}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF349ABB-598D-4F46-A10C-09AE7F697977}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF349ABB-598D-4F46-A10C-09AE7F697977}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF349ABB-598D-4F46-A10C-09AE7F697977}.Release|Any CPU.Build.0 = Release|Any CPU
{AABF8712-9B04-4EC9-B221-423352DA2993}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AABF8712-9B04-4EC9-B221-423352DA2993}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AABF8712-9B04-4EC9-B221-423352DA2993}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AABF8712-9B04-4EC9-B221-423352DA2993}.Release|Any CPU.Build.0 = Release|Any CPU
{41FA4292-92B0-4810-8127-6E70E2073D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41FA4292-92B0-4810-8127-6E70E2073D38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41FA4292-92B0-4810-8127-6E70E2073D38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41FA4292-92B0-4810-8127-6E70E2073D38}.Release|Any CPU.Build.0 = Release|Any CPU
{54220F73-24DE-47BB-A027-5326E90A59B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54220F73-24DE-47BB-A027-5326E90A59B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54220F73-24DE-47BB-A027-5326E90A59B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54220F73-24DE-47BB-A027-5326E90A59B6}.Release|Any CPU.Build.0 = Release|Any CPU
{4BC964A3-91C9-47FD-9A78-1E43301E9779}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F98EC0ED-E78E-4908-A00E-5D9F45D88E33}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BA4E2E64-12B4-4AA3-903D-10251D940662} = {E23C57D5-1527-4482-963B-374CF1A098D5}
{7D19503F-122B-4FDF-89DF-475DF0ACAAD7} = {E23C57D5-1527-4482-963B-374CF1A098D5}
{CF349ABB-598D-4F46-A10C-09AE7F697977} = {EFBD13EE-AF89-4792-A3DE-FF38BDA38DD0}
{AABF8712-9B04-4EC9-B221-423352DA2993} = {EFBD13EE-AF89-4792-A3DE-FF38BDA38DD0}
{41FA4292-92B0-4810-8127-6E70E2073D38} = {E23C57D5-1527-4482-963B-374CF1A098D5}
{54220F73-24DE-47BB-A027-5326E90A59B6} = {E23C57D5-1527-4482-963B-374CF1A098D5}
{4BC964A3-91C9-47FD-9A78-1E43301E9779} = {EFBD13EE-AF89-4792-A3DE-FF38BDA38DD0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {626ACE64-EFF9-4C9A-84DC-DFF35D76AF59}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="SecureBank.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="@InteractiveServer" />
</head>
<body>
<Routes @rendermode="@InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="/auth">Login</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,18 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">SecureBank</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-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-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-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-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-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-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");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,133 @@
@page "/admin/create-account"
<h3>Create new client account</h3>
@if (_id is null)
{
<EditForm Model="@_data" OnSubmit="Submit">
<div class="form-group">
<label for="first-name-input">
First name:
</label>
<InputText id="first-name-input" class="form-control" @bind-Value="@_data.FirstName"></InputText>
</div>
<div class="form-group">
<label for="last-name-input">
Last name:
</label>
<InputText id="last-name-input" class="form-control" @bind-Value="@_data.LastName"></InputText>
</div>
<div class="form-group">
<label for="email-input">
Email address:
</label>
<InputText id="email-input" class="form-control" @bind-Value="@_data.Email"></InputText>
</div>
<div class="form-group">
<label for="phone-number-input">
Phone number:
</label>
<InputText id="phone-number-input" class="form-control" @bind-Value="@_data.PhoneNumber"></InputText>
</div>
<br />
<button class="btn btn-primary" type="submit">Submit</button>
@if (!string.IsNullOrWhiteSpace(_message))
{
<p class="text-red">Error: @_message</p>
}
</EditForm>
}
else
{
<p><strong>New client account was created</strong></p>
<table>
<tr>
<td><strong>Client code:</strong></td>
<td>@($"{_id:00000000}")</td>
</tr>
<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>Password:</strong></td>
<td>******** (Information passed on to the client - LOG)</td>
</tr>
</table>
<br/>
<button class="btn btn-secondary" @onclick="NavigateToAdminPanel">Go back to admin panel</button>
<button class="btn btn-primary" @onclick="NavigateToNewForm">Create next account</button>
}
@code {
#region SERVICES
[Inject]
protected IAccountsService _accountsService { get; set; }
[Inject]
protected NavigationManager _navigationManager { get; set; }
#endregion
#region FIELDS
protected CreateAccountRequest _data;
protected int? _id;
protected string? _message;
#endregion
#region METHODS
protected override void OnInitialized()
{
_data = new CreateAccountRequest();
_id = null;
_message = null;
}
protected async Task Submit()
{
APIResponse<int> response = await _accountsService.CreateAccount(_data);
if (response.Success)
{
_id = response.Data;
}
else
{
_message = response.Message;
}
}
protected void NavigateToNewForm()
{
OnInitialized();
}
protected void NavigateToAdminPanel()
{
_navigationManager.NavigateTo("/"); //TODO: Zmienić na /admin
}
#endregion
}

View File

@@ -0,0 +1,152 @@
@page "/auth"
<h3>Login</h3>
@if (!_clientCodeAccepted)
{
<p>Enter your client code:</p>
<form>
<InputText @bind-Value="_clientCodeArr[0]" class="single-input" data-index="1" maxlength="1" required></InputText>
<InputText @bind-Value="_clientCodeArr[1]" class="single-input" data-index="2" maxlength="1" required></InputText>
<InputText @bind-Value="_clientCodeArr[2]" class="single-input" data-index="3" maxlength="1" required></InputText>
<InputText @bind-Value="_clientCodeArr[3]" class="single-input" data-index="4" maxlength="1" required></InputText>
<InputText @bind-Value="_clientCodeArr[4]" class="single-input" data-index="5" maxlength="1" required></InputText>
<InputText @bind-Value="_clientCodeArr[5]" class="single-input" data-index="6" maxlength="1" required></InputText>
<InputText @bind-Value="_clientCodeArr[6]" class="single-input" data-index="7" maxlength="1" required></InputText>
<InputText @bind-Value="_clientCodeArr[7]" class="single-input" data-index="8" maxlength="1" required></InputText>
</form>
<br />
<button type="submit" class="btn btn-primary" @onclick="SubmitClientCode">Next</button>
<br/>
<br/>
@if (!string.IsNullOrWhiteSpace(_clientCodeMessage))
{
<p class="text-red">Error: @_clientCodeMessage</p>
}
}
else
{
<p>Enter your password:</p>
<form>
<InputText type="password" @bind-Value="_clientPasswordArr[0]" class="single-input" data-index="1" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(0))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[1]" class="single-input" data-index="2" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(1))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[2]" class="single-input" data-index="3" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(2))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[3]" class="single-input" data-index="4" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(3))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[4]" class="single-input" data-index="5" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(4))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[5]" class="single-input" data-index="6" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(5))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[6]" class="single-input" data-index="7" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(6))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[7]" class="single-input" data-index="8" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(7))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[8]" class="single-input" data-index="9" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(8))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[9]" class="single-input" data-index="10" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(9))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[10]" class="single-input" data-index="11" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(10))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[11]" class="single-input" data-index="12" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(11))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[12]" class="single-input" data-index="13" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(12))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[13]" class="single-input" data-index="14" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(13))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[14]" class="single-input" data-index="15" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(14))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[15]" class="single-input" data-index="16" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(15))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[16]" class="single-input" data-index="17" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(16))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[17]" class="single-input" data-index="18" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(17))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[18]" class="single-input" data-index="19" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(18))"></InputText>
<InputText type="password" @bind-Value="_clientPasswordArr[19]" class="single-input" data-index="20" maxlength="1" disabled="@(!_loginRequest?.Indexes.Contains(19))"></InputText>
</form>
<br />
<button class="btn" @onclick="OnInitialized">Back</button>
<button type="submit" class="btn btn-primary" @onclick="SubmitClientPassword">Submit</button>
@if (!string.IsNullOrWhiteSpace(_clientPasswordMessage))
{
<p class="text-red">Error: @_clientPasswordMessage</p>
}
}
@code {
#region SERVICES
[Inject]
protected IAccountsService _accountService { get; set; }
#endregion
#region FIELDS
protected bool _clientCodeAccepted;
protected string? _clientCodeMessage;
protected string[] _clientCodeArr;
protected string _clientCode => string.Join(string.Empty, _clientCodeArr);
protected string[] _clientPasswordArr;
protected string _clientPassword => string.Join(string.Empty, _clientPasswordArr);
protected byte _clientPasswordFailCount;
protected string? _clientPasswordMessage;
protected GetPasswordVariantResponse? _loginRequest;
#endregion
#region METHODS
protected override void OnInitialized()
{
_clientCodeAccepted = false;
_clientCodeMessage = null;
_clientCodeArr = new string[8];
_clientPasswordArr = new string[20];
_clientPasswordMessage = null;
_loginRequest = null;
}
protected async void SubmitClientCode()
{
if (_clientCode.Length == 8 && int.TryParse(_clientCode, out int accountId))
{
APIResponse<GetPasswordVariantResponse> loginRequest = await _accountService.GetPasswordVariant(accountId);
if (loginRequest.Success)
{
if (loginRequest.Data.ValidTo < DateTime.Now)
{
_clientCodeMessage = "Your login request has already expired. Check your internet connection";
}
else
{
_clientCodeAccepted = true;
_loginRequest = loginRequest.Data;
}
}
else
{
_clientCodeMessage = loginRequest.Message;
}
}
else
{
_clientCodeMessage = "Wrong client code format";
}
StateHasChanged();
}
protected void SubmitClientPassword()
{
if (_clientPassword.Length == _loginRequest.Indexes.Length)
{
}
else
{
_clientPasswordMessage = "Not all fields were filled";
}
}
#endregion
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>

View File

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

172
SecureBank/Program.cs Normal file
View File

@@ -0,0 +1,172 @@
using Blazored.SessionStorage;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using SecureBank.API.Authentication;
using SecureBank.Components;
using SecureBank.Database;
using SecureBank.Website.API;
using SecureBank.Website.Authentication;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace SecureBank;
public class Program
{
#region FIELDS
protected static WebApplicationBuilder _builder;
#endregion
#region PUBLIC METHODS
public static void Main(string[] args)
{
_builder = WebApplication.CreateBuilder(args);
// Logging
_builder.Logging.ClearProviders();
_builder.Logging.AddConsole();
// Database
_builder.Services.AddDbContext<DatabaseContext>(x =>
{
x.UseSqlite(_builder.Configuration.GetConnectionString("Default"));
}, ServiceLifetime.Singleton);
// API
BuildAPI();
// Website
BuildWebsite();
var app = _builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
else
{
app.UseSwagger(x =>
{
x.RouteTemplate = "api/swagger/{documentname}/swagger.json";
});
app.UseSwaggerUI(x =>
{
x.SwaggerEndpoint("/api/swagger/v1/swagger.json", "SecureBank API");
x.RoutePrefix = "api/swagger";
});
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
}
#endregion
#region PRIVATE METHODS
protected static void BuildAPI()
{
_builder.Services.AddControllers();
_builder.Services.AddEndpointsApiExplorer();
_builder.Services.AddSwaggerGen();
// Authentication
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
AuthenticationBuilder auth = _builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
});
auth.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_builder.Configuration.GetValue<string>("Authentication:Token:Key"))),
ValidIssuer = _builder.Configuration.GetValue<string>("Authentication:Token:Issuer"),
ValidAudience = _builder.Configuration.GetValue<string>("Authentication:Token:Audience"),
ClockSkew = TimeSpan.FromMinutes(1),
};
x.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
_builder.Services.AddAuthorization();
// Configurations
_builder.Services.AddSingleton<AuthenticationConfiguration>();
// Helpers
_builder.Services.AddSingleton<API.Authentication.AuthenticationHelper>();
// Services
_builder.Services.AddSingleton<API.Services.IAccountsService, API.Services.AccountsService>();
}
protected static void BuildWebsite()
{
_builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Clients
_builder.Services.AddSingleton<HttpClient>();
_builder.Services.AddSingleton<APIClient>();
// Storage
_builder.Services.AddBlazoredSessionStorage();
// Authentication
_builder.Services.AddScoped<Website.Authentication.AuthenticationHelper>();
_builder.Services.AddAuthorizationCore();
_builder.Services.AddScoped<TokenAuthenticationStateProvider>();
// Configurations
_builder.Services.AddSingleton<APIEndpointsConfiguration>();
// Services
_builder.Services.AddSingleton<Website.Services.IAccountsService, Website.Services.AccountsService>();
}
#endregion
}

View File

@@ -0,0 +1,49 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5043"
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7143;http://localhost:5043"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {
"ASPNETCORE_HTTPS_PORTS": "443",
"ASPNETCORE_HTTP_PORTS": "80"
},
"publishAllPorts": true,
"useSSL": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:42964",
"sslPort": 44330
}
}
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>68c8748d-7175-410d-8bd6-a8ee07e58478</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileFile>..\Dockerfile</DockerfileFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
<PackageReference Include="Swashbuckle.AspNetCore" 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.SwaggerUI" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<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.Services\SecureBank.API.Services.csproj" />
<ProjectReference Include="..\SecureBank.API\SecureBank.API.Token\SecureBank.API.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\SecureBank.Database\SecureBank.Database.csproj" />
<ProjectReference Include="..\SecureBank.Website\SecureBank.Website.Authentication\SecureBank.Website.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.Website\SecureBank.Website.Services\SecureBank.Website.Services.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,30 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Authentication": {
"Token": {
"Key": "testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytest",
"Issuer": "SecureBank",
"Audience": "access",
"Lifetime": "5"
}
},
"ConnectionStrings": {
"Default": "Data Source = ../database.db"
},
"Endpoints": {
"Base": "https://localhost:7143/api",
"Accounts": {
"Base": "/accounts",
"CreateAccount": "/create-account",
"AuthenticationRefresh": "/authentication-refresh",
"GetPasswordVariant": "/{0}/password-variant",
"Authentication": "/{0}/authentication"
}
}
}

View File

@@ -0,0 +1,59 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.single-input {
width: 20px;
}
.text-red {
color: red;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB