mirror of
https://github.com/azaion/admin.git
synced 2026-04-22 10:26:34 +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