mirror of
https://github.com/azaion/admin.git
synced 2026-04-22 06:56:32 +00:00
Init commit
add security encryption and hashing: WIP add endpoints: register user, get and save resources add db main operations, User entity
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.idea
|
||||
bin
|
||||
obj
|
||||
.vs
|
||||
*.DotSettings*
|
||||
*.user
|
||||
log*
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Api", "Azaion.Api\Azaion.Api.csproj", "{03A56CF2-A57F-4631-8454-C08B804B8903}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Common", "Azaion.Common\Azaion.Common.csproj", "{E838FA94-B96D-4446-B5D6-6BC1A34436C1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Services", "Azaion.Services\Azaion.Services.csproj", "{07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Test", "Azaion.Test\Azaion.Test.csproj", "{2F4F0EA9-0645-4917-8D21-F317E815EB9E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{03A56CF2-A57F-4631-8454-C08B804B8903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{03A56CF2-A57F-4631-8454-C08B804B8903}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{03A56CF2-A57F-4631-8454-C08B804B8903}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{03A56CF2-A57F-4631-8454-C08B804B8903}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
|
||||
<ProjectReference Include="..\Azaion.Services\Azaion.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@Azaion.Api_HostAddress = http://localhost:5219
|
||||
|
||||
GET {{Azaion.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,48 @@
|
||||
using Azaion.Common.Configs;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.Requests;
|
||||
using Azaion.Services;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.Configure<ResourcesConfig>(builder.Configuration.GetSection(nameof(ResourcesConfig)));
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<IResourcesService, ResourcesService>();
|
||||
|
||||
builder.Services.AddSingleton<IDbFactory, DbFactory>(sp => new DbFactory(sp.GetService<IOptions<ConnectionStrings>>()!.Value.AzaionDb));
|
||||
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<RegisterUserValidator>();
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.MapPost("/register-user",
|
||||
async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken)
|
||||
=> await userService.RegisterUser(registerUserRequest, cancellationToken));
|
||||
|
||||
app.MapPost("/resources/get",
|
||||
async (GetResourceRequest getResourceRequest, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) =>
|
||||
{
|
||||
await userService.ValidateUser(getResourceRequest, cancellationToken);
|
||||
var ms = new MemoryStream();
|
||||
await resourcesService.GetEncryptedResource(getResourceRequest, ms, cancellationToken);
|
||||
return ms;
|
||||
});
|
||||
|
||||
app.MapPost("/resources",
|
||||
async (UploadResourceRequest uploadResourceRequest, IResourcesService resourceService, CancellationToken cancellationToken)
|
||||
=> await resourceService.SaveResource(uploadResourceRequest, cancellationToken));
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:10133",
|
||||
"sslPort": 44330
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5219",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7139;http://localhost:5219",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ResourcesConfig": {
|
||||
"ResourcesFolder": "Content",
|
||||
"Resources": {
|
||||
"AnnotatorDll": "Azaion.Annotator.dll",
|
||||
"AIModelONNX": "azaion.onnx",
|
||||
"AIModelRKNN": "azaion.rknn"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="11.10.0" />
|
||||
<PackageReference Include="linq2db" Version="5.4.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Azaion.Common;
|
||||
|
||||
public class BusinessException(ExceptionEnum exEnum, string message) : Exception(message)
|
||||
{
|
||||
private ExceptionEnum ExceptionEnum { get; set; } = exEnum;
|
||||
}
|
||||
|
||||
public enum ExceptionEnum
|
||||
{
|
||||
NoUserFound = 10,
|
||||
UserExists = 20,
|
||||
PasswordIncorrect = 30,
|
||||
UserLengthIncorrect = 33,
|
||||
WrongEmail = 35,
|
||||
PasswordLengthIncorrect = 37,
|
||||
HardwareIdMismatch = 40,
|
||||
WrongResourceType = 50
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Azaion.Common.Configs;
|
||||
|
||||
public class ConnectionStrings
|
||||
{
|
||||
public string AzaionDb { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Azaion.Common.Configs;
|
||||
|
||||
public class ResourcesConfig
|
||||
{
|
||||
public string ResourcesFolder { get; set; } = null!;
|
||||
public Dictionary<string, string> Resources { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Azaion.Common.Entities;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
|
||||
namespace Azaion.Common.Database;
|
||||
|
||||
public class AzaionDb(DataOptions dataOptions) : DataConnection(dataOptions)
|
||||
{
|
||||
public ITable<User> Users => this.GetTable<User>();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Azaion.Common.Entities;
|
||||
using LinqToDB.Mapping;
|
||||
|
||||
namespace Azaion.Common.Database;
|
||||
|
||||
public static class AzaionDbSchemaHolder
|
||||
{
|
||||
public static readonly MappingSchema MappingSchema;
|
||||
|
||||
static AzaionDbSchemaHolder()
|
||||
{
|
||||
MappingSchema = new MappingSchema();
|
||||
var builder = new FluentMappingBuilder(MappingSchema);
|
||||
|
||||
builder.Entity<User>()
|
||||
.HasTableName("users")
|
||||
.HasIdentity(x => x.Id);
|
||||
|
||||
builder.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Diagnostics;
|
||||
using LinqToDB;
|
||||
|
||||
namespace Azaion.Common.Database;
|
||||
|
||||
public interface IDbFactory
|
||||
{
|
||||
Task<T> Run<T>(Func<AzaionDb, Task<T>> func);
|
||||
Task Run(Func<AzaionDb, Task> func);
|
||||
|
||||
T Run<T>(Func<AzaionDb, T> func);
|
||||
}
|
||||
|
||||
public class DbFactory : IDbFactory
|
||||
{
|
||||
private readonly DataOptions _dataOptions;
|
||||
|
||||
public DbFactory(string connectionString, bool useTracing = true, bool msSql = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
throw new ArgumentException("Empty connectionString", nameof(connectionString));
|
||||
|
||||
_dataOptions = new DataOptions()
|
||||
.UsePostgreSQL(connectionString)
|
||||
.UseMappingSchema(AzaionDbSchemaHolder.MappingSchema);
|
||||
|
||||
if (useTracing)
|
||||
_ = _dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText));
|
||||
}
|
||||
|
||||
public async Task<T> Run<T>(Func<AzaionDb, Task<T>> func)
|
||||
{
|
||||
await using var db = new AzaionDb(_dataOptions);
|
||||
return await func(db);
|
||||
}
|
||||
|
||||
public async Task Run(Func<AzaionDb, Task> func)
|
||||
{
|
||||
await using var db = new AzaionDb(_dataOptions);
|
||||
await func(db);
|
||||
}
|
||||
|
||||
public T Run<T>(Func<AzaionDb, T> func)
|
||||
{
|
||||
using var db = new AzaionDb(_dataOptions);
|
||||
return func(db);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Azaion.Common.Entities;
|
||||
|
||||
public enum ResourceEnum
|
||||
{
|
||||
AnnotatorDll = 10,
|
||||
AIModelRKNN = 20,
|
||||
AIModelONNX = 20,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Azaion.Common;
|
||||
|
||||
public enum RoleEnum
|
||||
{
|
||||
Operator,
|
||||
Validator,
|
||||
CompanionPC,
|
||||
Admin
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Azaion.Common.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public string Id { get; set; } = null!;
|
||||
public string Username { get; set; } = null!;
|
||||
public string PasswordHash { get; set; } = null!;
|
||||
public string HardwareId { get; set; } = null!;
|
||||
public RoleEnum Role { get; set; }
|
||||
|
||||
public string UniqueKey => $"Azaion#{Username}#{PasswordHash}#{HardwareId}";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Azaion.Common.Entities;
|
||||
|
||||
namespace Azaion.Common.Requests;
|
||||
|
||||
public class GetResourceRequest
|
||||
{
|
||||
public string Username { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
public string HardwareId { get; set; } = null!;
|
||||
public ResourceEnum ResourceEnum { get; set; }
|
||||
}
|
||||
|
||||
public class UploadResourceRequest
|
||||
{
|
||||
public ResourceEnum ResourceEnum { get; set; }
|
||||
public Stream Data { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace Azaion.Common.Requests;
|
||||
|
||||
public class RegisterUserRequest
|
||||
{
|
||||
public string Email { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
public RoleEnum Role { get; set; }
|
||||
}
|
||||
|
||||
public class RegisterUserValidator : AbstractValidator<RegisterUserRequest>
|
||||
{
|
||||
public RegisterUserValidator()
|
||||
{
|
||||
RuleFor(r => r.Email)
|
||||
.MinimumLength(8).WithErrorCode(ExceptionEnum.UserLengthIncorrect.ToString()).WithMessage("Email address should be at least 8 characters.")
|
||||
.EmailAddress().WithErrorCode(ExceptionEnum.WrongEmail.ToString()).WithMessage("Email address is not valid.");
|
||||
|
||||
RuleFor(r => r.Password)
|
||||
.MinimumLength(8).WithErrorCode(ExceptionEnum.PasswordLengthIncorrect.ToString()).WithMessage("Password should be at least 8 characters.");
|
||||
} }
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Options">
|
||||
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,37 @@
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.Configs;
|
||||
using Azaion.Common.Entities;
|
||||
using Azaion.Common.Requests;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Azaion.Services;
|
||||
|
||||
public interface IResourcesService
|
||||
{
|
||||
Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default);
|
||||
Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class ResourcesService(IOptions<ResourcesConfig> resourcesConfig) : IResourcesService
|
||||
{
|
||||
public async Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fileStream = new FileStream(GetResourcePath(request.ResourceEnum), FileMode.Open, FileAccess.Read);
|
||||
var key = Security.MakeEncryptionKey(request.Username, request.Password);
|
||||
await fileStream.Encrypt(outputStream, key, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var fileStream = new FileStream(GetResourcePath(request.ResourceEnum), FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
await request.Data.CopyToAsync(fileStream, cancellationToken);
|
||||
}
|
||||
|
||||
private string GetResourcePath(ResourceEnum resourceEnum)
|
||||
{
|
||||
var resource = resourcesConfig.Value.Resources.GetValueOrDefault(resourceEnum.ToString());
|
||||
if (resource == null)
|
||||
throw new BusinessException(ExceptionEnum.WrongResourceType, "Wrong resource type!");
|
||||
return Path.Combine(resourcesConfig.Value.ResourcesFolder, resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Azaion.Services;
|
||||
|
||||
public static class Security
|
||||
{
|
||||
private const int BUFFER_SIZE = 81920; // 80 KB buffer size
|
||||
|
||||
public static string ToHash(this string str) =>
|
||||
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
|
||||
|
||||
public static string MakeEncryptionKey(string username, string password) =>
|
||||
$"{username}-{password}---#%@AzaionKey@%#---";
|
||||
|
||||
public static async Task Encrypt(this Stream stream, Stream outputStream, string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (stream is { CanSeek: false }) throw new ArgumentNullException(nameof(stream));
|
||||
if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key));
|
||||
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
aes.GenerateIV();
|
||||
|
||||
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||
await using var cs = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write);
|
||||
|
||||
// Prepend IV to the encrypted data
|
||||
await outputStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken);
|
||||
|
||||
var buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task Decrypt(this Stream encryptedStream, Stream outputStream, string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
|
||||
// Read the IV from the start of the input stream
|
||||
var iv = new byte[aes.BlockSize / 8];
|
||||
_ = await encryptedStream.ReadAsync(iv, cancellationToken);
|
||||
aes.IV = iv;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||
await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read);
|
||||
|
||||
// Read and write in chunks
|
||||
var buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.Entities;
|
||||
using Azaion.Common.Requests;
|
||||
using LinqToDB;
|
||||
|
||||
namespace Azaion.Services;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default);
|
||||
Task<User?> ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class UserService(IDbFactory dbFactory) : IUserService
|
||||
{
|
||||
public async Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await dbFactory.Run(async db =>
|
||||
{
|
||||
var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Username == request.Email, token: cancellationToken);
|
||||
if (existingUser != null)
|
||||
throw new BusinessException(ExceptionEnum.UserExists, "User already exists");
|
||||
|
||||
await db.InsertAsync(new User
|
||||
{
|
||||
Username = request.Email,
|
||||
PasswordHash = request.Password.ToHash(),
|
||||
Role = request.Role
|
||||
}, token: cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<User?> ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default) =>
|
||||
await dbFactory.Run(async db =>
|
||||
{
|
||||
var user = await db.Users.FirstOrDefaultAsync(x => x.Username == request.Username, token: cancellationToken);
|
||||
if (user == null)
|
||||
throw new BusinessException(ExceptionEnum.NoUserFound, "No user found");
|
||||
|
||||
if (request.Password.ToHash() != user.PasswordHash)
|
||||
throw new BusinessException(ExceptionEnum.PasswordIncorrect, "Passwords do not match");
|
||||
|
||||
//If user's hardware Id is empty (usually on the first time login), then write down user
|
||||
if (string.IsNullOrEmpty(user.HardwareId))
|
||||
await db.Users.UpdateAsync(x => x.Username == request.Username, u => new User{HardwareId = request.HardwareId}, token: cancellationToken);
|
||||
else
|
||||
{
|
||||
//But if hardware Id exists, it should match with request
|
||||
if (user.HardwareId != request.HardwareId)
|
||||
throw new BusinessException(ExceptionEnum.HardwareIdMismatch, "Hardware id mismatch");
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Azaion.Services\Azaion.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Text;
|
||||
using Azaion.Services;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Test;
|
||||
|
||||
public class SecurityTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task EncryptDecryptTest()
|
||||
{
|
||||
var testString = "Hello World Test dfvjkhsdbfvkljh sabdljsdafv asdv";
|
||||
var username = "user@azaion.com";
|
||||
var password = "testpw";
|
||||
var key = Security.MakeEncryptionKey(username, password);
|
||||
|
||||
await using var encryptedStream = new MemoryStream();
|
||||
await StringToStream(testString).Encrypt(encryptedStream, key);
|
||||
|
||||
await using var decryptedStream = new MemoryStream();
|
||||
await encryptedStream.Decrypt(decryptedStream, key);
|
||||
|
||||
var str = StreamToString(decryptedStream);
|
||||
str.Should().Be(testString);
|
||||
}
|
||||
|
||||
private static string StreamToString(Stream stream)
|
||||
{
|
||||
stream.Position = 0;
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
private static Stream StringToStream(string src)
|
||||
{
|
||||
var byteArray = Encoding.UTF8.GetBytes(src);
|
||||
return new MemoryStream(byteArray);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user