init
This commit is contained in:
30
.dockerignore
Normal file
30
.dockerignore
Normal 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
3
.gitignore
vendored
@@ -396,3 +396,6 @@ FodyWeavers.xsd
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
# SQLite database
|
||||
*.db
|
||||
35
Dockerfile
Normal file
35
Dockerfile
Normal 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"]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"SecureBank.API": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
14
SecureBank.API/SecureBank.API.Helpers/Check.cs
Normal file
14
SecureBank.API/SecureBank.API.Helpers/Check.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
383
SecureBank.API/SecureBank.API.Services/AccountsService.cs
Normal file
383
SecureBank.API/SecureBank.API.Services/AccountsService.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
32
SecureBank.Common/APIResponse.cs
Normal file
32
SecureBank.Common/APIResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
21
SecureBank.Common/Accounts/AuthenticationRequest.cs
Normal file
21
SecureBank.Common/Accounts/AuthenticationRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
29
SecureBank.Common/Accounts/CreateAccountRequest.cs
Normal file
29
SecureBank.Common/Accounts/CreateAccountRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
25
SecureBank.Common/Accounts/GetPasswordVariantResponse.cs
Normal file
25
SecureBank.Common/Accounts/GetPasswordVariantResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
13
SecureBank.Common/SecureBank.Common.csproj
Normal file
13
SecureBank.Common/SecureBank.Common.csproj
Normal 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>
|
||||
52
SecureBank.Database/Account.cs
Normal file
52
SecureBank.Database/Account.cs
Normal 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
|
||||
}
|
||||
}
|
||||
35
SecureBank.Database/AccountLoginRequest.cs
Normal file
35
SecureBank.Database/AccountLoginRequest.cs
Normal 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
|
||||
}
|
||||
}
|
||||
44
SecureBank.Database/AccountPassword.cs
Normal file
44
SecureBank.Database/AccountPassword.cs
Normal 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
|
||||
}
|
||||
}
|
||||
36
SecureBank.Database/AccountPasswordIndex.cs
Normal file
36
SecureBank.Database/AccountPasswordIndex.cs
Normal 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
|
||||
}
|
||||
}
|
||||
39
SecureBank.Database/DatabaseContext.cs
Normal file
39
SecureBank.Database/DatabaseContext.cs
Normal 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
|
||||
}
|
||||
}
|
||||
132
SecureBank.Database/Migrations/20240114165546_Migration1.Designer.cs
generated
Normal file
132
SecureBank.Database/Migrations/20240114165546_Migration1.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
94
SecureBank.Database/Migrations/20240114165546_Migration1.cs
Normal file
94
SecureBank.Database/Migrations/20240114165546_Migration1.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
138
SecureBank.Database/Migrations/20240114170227_Migration2.Designer.cs
generated
Normal file
138
SecureBank.Database/Migrations/20240114170227_Migration2.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
SecureBank.Database/Migrations/20240114170227_Migration2.cs
Normal file
39
SecureBank.Database/Migrations/20240114170227_Migration2.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
169
SecureBank.Database/Migrations/20240115084527_Migration3.Designer.cs
generated
Normal file
169
SecureBank.Database/Migrations/20240115084527_Migration3.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
46
SecureBank.Database/Migrations/20240115084527_Migration3.cs
Normal file
46
SecureBank.Database/Migrations/20240115084527_Migration3.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
173
SecureBank.Database/Migrations/20240115132220_Migration4.Designer.cs
generated
Normal file
173
SecureBank.Database/Migrations/20240115132220_Migration4.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
29
SecureBank.Database/Migrations/20240115132220_Migration4.cs
Normal file
29
SecureBank.Database/Migrations/20240115132220_Migration4.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
170
SecureBank.Database/Migrations/DatabaseContextModelSnapshot.cs
Normal file
170
SecureBank.Database/Migrations/DatabaseContextModelSnapshot.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
SecureBank.Database/SecureBank.Database.csproj
Normal file
23
SecureBank.Database/SecureBank.Database.csproj
Normal 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>
|
||||
78
SecureBank.Extensions/IEnumerableExtensions.cs
Normal file
78
SecureBank.Extensions/IEnumerableExtensions.cs
Normal 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
|
||||
}
|
||||
}
|
||||
25
SecureBank.Extensions/IListExtensions.cs
Normal file
25
SecureBank.Extensions/IListExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
SecureBank.Extensions/SecureBank.Extensions.csproj
Normal file
9
SecureBank.Extensions/SecureBank.Extensions.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
31
SecureBank.Extensions/StringExtensions.cs
Normal file
31
SecureBank.Extensions/StringExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
SecureBank.Helpers/AuthenticationHelper.cs
Normal file
39
SecureBank.Helpers/AuthenticationHelper.cs
Normal 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
|
||||
}
|
||||
}
|
||||
9
SecureBank.Helpers/SecureBank.Helpers.csproj
Normal file
9
SecureBank.Helpers/SecureBank.Helpers.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
107
SecureBank.Website/SecureBank.Website.API/APIClient.cs
Normal file
107
SecureBank.Website/SecureBank.Website.API/APIClient.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
16
SecureBank.Website/SecureBank.Website.API/APIMethodType.cs
Normal file
16
SecureBank.Website/SecureBank.Website.API/APIMethodType.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
111
SecureBank.sln
Normal 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
|
||||
20
SecureBank/Components/App.razor
Normal file
20
SecureBank/Components/App.razor
Normal 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>
|
||||
23
SecureBank/Components/Layout/MainLayout.razor
Normal file
23
SecureBank/Components/Layout/MainLayout.razor
Normal 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>
|
||||
96
SecureBank/Components/Layout/MainLayout.razor.css
Normal file
96
SecureBank/Components/Layout/MainLayout.razor.css
Normal 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;
|
||||
}
|
||||
18
SecureBank/Components/Layout/NavMenu.razor
Normal file
18
SecureBank/Components/Layout/NavMenu.razor
Normal 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>
|
||||
|
||||
105
SecureBank/Components/Layout/NavMenu.razor.css
Normal file
105
SecureBank/Components/Layout/NavMenu.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
133
SecureBank/Components/Pages/Admin/Register.razor
Normal file
133
SecureBank/Components/Pages/Admin/Register.razor
Normal 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
|
||||
}
|
||||
152
SecureBank/Components/Pages/Auth.razor
Normal file
152
SecureBank/Components/Pages/Auth.razor
Normal 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
|
||||
}
|
||||
36
SecureBank/Components/Pages/Error.razor
Normal file
36
SecureBank/Components/Pages/Error.razor
Normal 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;
|
||||
}
|
||||
7
SecureBank/Components/Pages/Home.razor
Normal file
7
SecureBank/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
6
SecureBank/Components/Routes.razor
Normal file
6
SecureBank/Components/Routes.razor
Normal 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>
|
||||
13
SecureBank/Components/_Imports.razor
Normal file
13
SecureBank/Components/_Imports.razor
Normal 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
172
SecureBank/Program.cs
Normal 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
|
||||
}
|
||||
49
SecureBank/Properties/launchSettings.json
Normal file
49
SecureBank/Properties/launchSettings.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
44
SecureBank/SecureBank.csproj
Normal file
44
SecureBank/SecureBank.csproj
Normal 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>
|
||||
8
SecureBank/appsettings.Development.json
Normal file
8
SecureBank/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
30
SecureBank/appsettings.json
Normal file
30
SecureBank/appsettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
SecureBank/wwwroot/app.css
Normal file
59
SecureBank/wwwroot/app.css
Normal 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;
|
||||
}
|
||||
7
SecureBank/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
7
SecureBank/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
SecureBank/wwwroot/bootstrap/bootstrap.min.css.map
Normal file
1
SecureBank/wwwroot/bootstrap/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
SecureBank/wwwroot/favicon.png
Normal file
BIN
SecureBank/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user