add postgres

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-12 15:57:36 +02:00
parent 85139b4fd2
commit 2336c15aa4
15 changed files with 224 additions and 78 deletions
+2 -1
View File
@@ -4,4 +4,5 @@ obj
.vs .vs
*.DotSettings* *.DotSettings*
*.user *.user
log* log*
*.cmd
+42 -33
View File
@@ -1,5 +1,3 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text; using System.Text;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.Configs; using Azaion.Common.Configs;
@@ -9,11 +7,10 @@ using Azaion.Common.Requests;
using Azaion.Services; using Azaion.Services;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables();
var jwtConfig = builder.Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>(); var jwtConfig = builder.Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
if (jwtConfig == null) if (jwtConfig == null)
@@ -38,12 +35,41 @@ builder.Services.AddAuthorization();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {Title = "Azaion.API", Version = "v1"});
c.CustomSchemaIds(type => type.ToString());
var jwtSecurityScheme = new OpenApiSecurityScheme
{
Scheme = "bearer",
BearerFormat = "JWT",
Name = "JWT Authentication",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Description = "Put **_ONLY_** your JWT Bearer token on textbox below!",
Reference = new OpenApiReference
{
Id = JwtBearerDefaults.AuthenticationScheme,
Type = ReferenceType.SecurityScheme
}
};
c.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme);
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ jwtSecurityScheme, Array.Empty<string>() }
});
});
builder.Services.Configure<ResourcesConfig>(builder.Configuration.GetSection(nameof(ResourcesConfig))); builder.Services.Configure<ResourcesConfig>(builder.Configuration.GetSection(nameof(ResourcesConfig)));
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection(nameof(JwtConfig)));
builder.Services.Configure<ConnectionStrings>(builder.Configuration.GetSection(nameof(ConnectionStrings)));
builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IResourcesService, ResourcesService>(); builder.Services.AddScoped<IResourcesService, ResourcesService>();
builder.Services.AddSingleton<IAuthService, AuthService>();
builder.Services.AddSingleton<IDbFactory, DbFactory>(sp => new DbFactory(sp.GetService<IOptions<ConnectionStrings>>()!.Value.AzaionDb)); builder.Services.AddSingleton<IDbFactory, DbFactory>();
builder.Services.AddValidatorsFromAssemblyContaining<RegisterUserValidator>(); builder.Services.AddValidatorsFromAssemblyContaining<RegisterUserValidator>();
@@ -61,29 +87,10 @@ app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapPost("/login", app.MapPost("/login",
async (string username, string password, IUserService userService, CancellationToken cancellationToken) => async (string username, string password, IUserService userService, IAuthService authService, CancellationToken cancellationToken) =>
{ {
var user = await userService.ValidateUser(username, password); var user = await userService.ValidateUser(username, password, cancellationToken: cancellationToken);
return Results.Ok(new { Token = authService.CreateToken(user)});
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity([
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.Email),
new Claim(ClaimTypes.Role, user.Role.ToString()),
new Claim(Constants.HARDWARE_ID, user.HardwareId)
]),
Expires = DateTime.UtcNow.AddHours(2),
Issuer = jwtConfig.Issuer,
Audience = jwtConfig.Audience,
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
return Results.Ok(new { Token = tokenString });
}); });
app.MapPost("/register-user", app.MapPost("/register-user",
@@ -97,13 +104,15 @@ app.MapPost("/resources",
.RequireAuthorization(p => p.RequireRole(RoleEnum.ApiAdmin.ToString())); .RequireAuthorization(p => p.RequireRole(RoleEnum.ApiAdmin.ToString()));
app.MapPost("/resources/get", app.MapPost("/resources/get",
async (GetResourceRequest request, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) => async (GetResourceRequest request, IAuthService authService, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) =>
{ {
var user = userService.CurrentUser; var user = authService.CurrentUser;
if (user == null) if (user?.HardwareId != request.HardwareId)
throw new BusinessException(ExceptionEnum.NoUser, "No current user"); throw new BusinessException(ExceptionEnum.HardwareIdMismatch, "Hardware mismatch! You are not authorized to access this resource from this hardware.");
if (string.IsNullOrEmpty(user.HardwareId)) if (string.IsNullOrEmpty(user.HardwareId))
await userService.UpdateHardwareId(user.Email, request.HardwareId); await userService.UpdateHardwareId(user.Email, request.HardwareId);
var ms = new MemoryStream(); var ms = new MemoryStream();
var key = Security.MakeEncryptionKey(user.Email, request.Password); var key = Security.MakeEncryptionKey(user.Email, request.Password);
await resourcesService.GetEncryptedResource(request.ResourceEnum, key, ms, cancellationToken); await resourcesService.GetEncryptedResource(request.ResourceEnum, key, ms, cancellationToken);
+4
View File
@@ -4,5 +4,9 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"ConnectionStrings": {
"AzaionDb": "Host=localhost;Database=azaion;Username=azaion_reader;Password=Az@1on_re@d!only@$Az;",
"AzaionDbAdmin": "Host=localhost;Database=azaion;Username=azaion_admin;Password=Az@1on_admin$$@r;"
} }
} }
+5
View File
@@ -13,5 +13,10 @@
"AIModelONNX": "azaion.onnx", "AIModelONNX": "azaion.onnx",
"AIModelRKNN": "azaion.rknn" "AIModelRKNN": "azaion.rknn"
} }
},
"JwtConfig": {
"Issuer": "AzaionApi",
"Audience": "Annotators/OrangePi/Admins",
"TokenLifetimeHours": 2.5
} }
} }
+7
View File
@@ -9,6 +9,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="11.10.0" /> <PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="linq2db" Version="5.4.1" /> <PackageReference Include="linq2db" Version="5.4.1" />
<PackageReference Include="Npgsql" Version="8.0.5" />
</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> </ItemGroup>
</Project> </Project>
@@ -3,4 +3,5 @@
public class ConnectionStrings public class ConnectionStrings
{ {
public string AzaionDb { get; set; } = null!; public string AzaionDb { get; set; } = null!;
public string AzaionDbAdmin { get; set; } = null!;
} }
+2 -1
View File
@@ -4,5 +4,6 @@ public class JwtConfig
{ {
public string Issuer { get; set; } = null!; public string Issuer { get; set; } = null!;
public string Audience { get; set; } = null!; public string Audience { get; set; } = null!;
public string Secret { get; set; } public string Secret { get; set; } = null!;
public double TokenLifetimeHours { get; set; }
} }
@@ -10,11 +10,19 @@ public static class AzaionDbSchemaHolder
static AzaionDbSchemaHolder() static AzaionDbSchemaHolder()
{ {
MappingSchema = new MappingSchema(); MappingSchema = new MappingSchema();
MappingSchema.EntityDescriptorCreatedCallback = (_, entityDescriptor) =>
{
foreach (var entityDescriptorColumn in entityDescriptor.Columns)
entityDescriptorColumn.ColumnName = entityDescriptorColumn.ColumnName.ToSnakeCase();
};
var builder = new FluentMappingBuilder(MappingSchema); var builder = new FluentMappingBuilder(MappingSchema);
builder.Entity<User>() builder.Entity<User>()
.HasTableName("users") .HasTableName("users")
.HasIdentity(x => x.Id); .HasIdentity(x => x.Id)
.Property(x => x.Role).HasConversion(v => v.ToString(), v => (RoleEnum)Enum.Parse(typeof(RoleEnum), v));
builder.Build(); builder.Build();
} }
+23 -14
View File
@@ -1,5 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using Azaion.Common.Configs;
using LinqToDB; using LinqToDB;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Database; namespace Azaion.Common.Database;
@@ -7,42 +9,49 @@ public interface IDbFactory
{ {
Task<T> Run<T>(Func<AzaionDb, Task<T>> func); Task<T> Run<T>(Func<AzaionDb, Task<T>> func);
Task Run(Func<AzaionDb, Task> func); Task Run(Func<AzaionDb, Task> func);
Task RunAdmin(Func<AzaionDb, Task> func);
T Run<T>(Func<AzaionDb, T> func);
} }
public class DbFactory : IDbFactory public class DbFactory : IDbFactory
{ {
private readonly DataOptions _dataOptions; private readonly DataOptions _dataOptions;
private readonly DataOptions _dataOptionsAdmin;
public DbFactory(string connectionString, bool useTracing = true, bool msSql = false) public DbFactory(IOptions<ConnectionStrings> connectionString)
{ {
if (string.IsNullOrEmpty(connectionString)) _dataOptions = LoadOptions(connectionString.Value.AzaionDb);
throw new ArgumentException("Empty connectionString", nameof(connectionString)); _dataOptionsAdmin = LoadOptions(connectionString.Value.AzaionDbAdmin);
}
_dataOptions = new DataOptions() private DataOptions LoadOptions(string connStr)
.UsePostgreSQL(connectionString) {
if (string.IsNullOrEmpty(connStr))
throw new ArgumentException($"Empty connection string in config!");
var dataOptions = new DataOptions()
.UsePostgreSQL(connStr)
.UseMappingSchema(AzaionDbSchemaHolder.MappingSchema); .UseMappingSchema(AzaionDbSchemaHolder.MappingSchema);
if (useTracing) _ = dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText));
_ = _dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText)); return dataOptions;
} }
public async Task<T> Run<T>(Func<AzaionDb, Task<T>> func) public async Task<T> Run<T>(Func<AzaionDb, Task<T>> func)
{ {
await using var db = new AzaionDb(_dataOptions); await using var db = new AzaionDb(_dataOptions);
return await func(db); return await func(db);
} }
public async Task Run(Func<AzaionDb, Task> func) public async Task Run(Func<AzaionDb, Task> func)
{ {
await using var db = new AzaionDb(_dataOptions); await using var db = new AzaionDb(_dataOptions);
await func(db); await func(db);
} }
public T Run<T>(Func<AzaionDb, T> func) public async Task RunAdmin(Func<AzaionDb, Task> func)
{ {
using var db = new AzaionDb(_dataOptions); await using var db = new AzaionDb(_dataOptionsAdmin);
return func(db); await func(db);
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@
public class User public class User
{ {
public string Id { get; set; } = null!; public Guid Id { get; set; }
public string Email { get; set; } = null!; public string Email { get; set; } = null!;
public string PasswordHash { get; set; } = null!; public string PasswordHash { get; set; } = null!;
public string HardwareId { get; set; } = null!; public string HardwareId { get; set; } = null!;
+28
View File
@@ -0,0 +1,28 @@
using System.Text;
namespace Azaion.Common;
public static class StringExtensions
{
public static string ToSnakeCase(this string text)
{
if (string.IsNullOrEmpty(text))
return text;
if (text.Length < 2)
return text.ToLowerInvariant();
var sb = new StringBuilder();
sb.Append(char.ToLowerInvariant(text[0]));
for (int i = 1; i < text.Length; ++i) {
var c = text[i];
if(char.IsUpper(c)) {
sb.Append('_');
sb.Append(char.ToLowerInvariant(c));
} else {
sb.Append(c);
}
}
return sb.ToString();
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Azaion.Common.Configs;
using Azaion.Common.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Azaion.Services;
public interface IAuthService
{
User? CurrentUser { get; }
string CreateToken(User user);
}
public class AuthService(IHttpContextAccessor httpContextAccessor, IOptions<JwtConfig> jwtConfig) : IAuthService
{
public User? CurrentUser
{
get
{
var claims = httpContextAccessor.HttpContext?.User.Claims.ToDictionary(x => x.Type);
if (claims == null)
return null;
if (!Enum.TryParse(claims[ClaimTypes.Role].Value, out RoleEnum role))
throw new ApplicationException("Invalid role");
return new User
{
Id = Guid.Parse(claims[ClaimTypes.NameIdentifier].Value),
Email = claims[ClaimTypes.Name].Value,
Role = role,
HardwareId = claims[Constants.HARDWARE_ID].Value,
};
}
}
public string CreateToken(User user)
{
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.Value.Secret));
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity([
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email),
new Claim(ClaimTypes.Role, user.Role.ToString()),
new Claim(Constants.HARDWARE_ID, user.HardwareId)
]),
Expires = DateTime.UtcNow.AddHours(jwtConfig.Value.TokenLifetimeHours),
Issuer = jwtConfig.Value.Issuer,
Audience = jwtConfig.Value.Audience,
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
+4
View File
@@ -19,4 +19,8 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
</ItemGroup>
</Project> </Project>
+4 -27
View File
@@ -1,46 +1,23 @@
using System.Security.Claims; using Azaion.Common;
using Azaion.Common;
using Azaion.Common.Configs;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.Entities; using Azaion.Common.Entities;
using Azaion.Common.Requests; using Azaion.Common.Requests;
using LinqToDB; using LinqToDB;
using Microsoft.AspNetCore.Http;
namespace Azaion.Services; namespace Azaion.Services;
public interface IUserService public interface IUserService
{ {
User? CurrentUser { get; }
Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default); Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default);
Task<User> ValidateUser(string username, string password, string? hardwareId = null, CancellationToken cancellationToken = default); Task<User> ValidateUser(string username, string password, string? hardwareId = null, CancellationToken cancellationToken = default);
Task UpdateHardwareId(string username, string hardwareId, CancellationToken cancellationToken = default); Task UpdateHardwareId(string username, string hardwareId, CancellationToken cancellationToken = default);
} }
public class UserService(IDbFactory dbFactory, IHttpContextAccessor httpContextAccessor) : IUserService public class UserService(IDbFactory dbFactory) : IUserService
{ {
public User? CurrentUser
{
get
{
var claims = httpContextAccessor.HttpContext?.User.Claims.ToDictionary(x => x.Type);
if (claims == null)
return null;
Enum.TryParse(claims[ClaimTypes.Role].Value, out RoleEnum role);
return new User
{
Id = claims[ClaimTypes.NameIdentifier].Value,
Email = claims[ClaimTypes.Name].Value,
Role = role,
HardwareId = claims[Constants.HARDWARE_ID].Value,
};
}
}
public async Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default) public async Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default)
{ {
await dbFactory.Run(async db => await dbFactory.RunAdmin(async db =>
{ {
var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Email == request.Email, token: cancellationToken); var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Email == request.Email, token: cancellationToken);
if (existingUser != null) if (existingUser != null)
@@ -75,6 +52,6 @@ public class UserService(IDbFactory dbFactory, IHttpContextAccessor httpContextA
}); });
public async Task UpdateHardwareId(string username, string hardwareId, CancellationToken cancellationToken = default) => public async Task UpdateHardwareId(string username, string hardwareId, CancellationToken cancellationToken = default) =>
await dbFactory.Run(async db => await dbFactory.RunAdmin(async db =>
await db.Users.UpdateAsync(x => x.Email == username, u => new User { HardwareId = hardwareId}, token: cancellationToken)); await db.Users.UpdateAsync(x => x.Email == username, u => new User { HardwareId = hardwareId}, token: cancellationToken));
} }
+29
View File
@@ -0,0 +1,29 @@
create database azaion;
-- make sure you connect to azaion db
--superadmin user
create role azaion_superadmin with login password 'superadmin_pass';
grant all privileges on all tables in schema public to azaion_superadmin;
--writer user
create role azaion_admin with login password 'admin_pass';
grant connect on database azaion to azaion_admin;
grant usage on schema public to azaion_admin;
--readonly user
create role azaion_reader with login password 'reader_pass';
grant connect on database azaion to azaion_reader;
grant usage on schema public to azaion_reader;
-- users table
create table users
(
id uuid primary key,
email varchar(160) not null,
password_hash varchar(255) not null,
hardware_id varchar(120) not null,
role varchar(20) not null
);
grant select, insert, update, delete on public.users to azaion_admin;
grant select on table public.users to azaion_reader;