mirror of
https://github.com/azaion/admin.git
synced 2026-04-22 11:16:33 +00:00
add authorization
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+76
-11
@@ -1,11 +1,41 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Azaion.Common;
|
||||||
using Azaion.Common.Configs;
|
using Azaion.Common.Configs;
|
||||||
using Azaion.Common.Database;
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.Entities;
|
||||||
using Azaion.Common.Requests;
|
using Azaion.Common.Requests;
|
||||||
using Azaion.Services;
|
using Azaion.Services;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
var jwtConfig = builder.Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
|
||||||
|
if (jwtConfig == null)
|
||||||
|
throw new Exception("Missing configuration section: JwtConfig");
|
||||||
|
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.Secret));
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(o =>
|
||||||
|
{
|
||||||
|
o.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = jwtConfig.Issuer,
|
||||||
|
ValidAudience = jwtConfig.Audience,
|
||||||
|
IssuerSigningKey = signingKey
|
||||||
|
};
|
||||||
|
});
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
@@ -27,22 +57,57 @@ if (app.Environment.IsDevelopment())
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapPost("/login",
|
||||||
|
async (string username, string password, IUserService userService, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var user = await userService.ValidateUser(username, password);
|
||||||
|
|
||||||
|
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",
|
||||||
async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken)
|
async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken)
|
||||||
=> await userService.RegisterUser(registerUserRequest, cancellationToken));
|
=> await userService.RegisterUser(registerUserRequest, cancellationToken))
|
||||||
|
.RequireAuthorization(p => p.RequireRole(RoleEnum.ApiAdmin.ToString()));
|
||||||
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",
|
app.MapPost("/resources",
|
||||||
async (UploadResourceRequest uploadResourceRequest, IResourcesService resourceService, CancellationToken cancellationToken)
|
async (UploadResourceRequest uploadResourceRequest, IResourcesService resourceService, CancellationToken cancellationToken)
|
||||||
=> await resourceService.SaveResource(uploadResourceRequest, cancellationToken));
|
=> await resourceService.SaveResource(uploadResourceRequest, cancellationToken))
|
||||||
|
.RequireAuthorization(p => p.RequireRole(RoleEnum.ApiAdmin.ToString()));
|
||||||
|
|
||||||
|
app.MapPost("/resources/get",
|
||||||
|
async (GetResourceRequest request, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var user = userService.CurrentUser;
|
||||||
|
if (user == null)
|
||||||
|
throw new BusinessException(ExceptionEnum.NoUser, "No current user");
|
||||||
|
if (string.IsNullOrEmpty(user.HardwareId))
|
||||||
|
await userService.UpdateHardwareId(user.Email, request.HardwareId);
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
var key = Security.MakeEncryptionKey(user.Email, request.Password);
|
||||||
|
await resourcesService.GetEncryptedResource(request.ResourceEnum, key, ms, cancellationToken);
|
||||||
|
return ms;
|
||||||
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -8,6 +8,7 @@ public class BusinessException(ExceptionEnum exEnum, string message) : Exception
|
|||||||
public enum ExceptionEnum
|
public enum ExceptionEnum
|
||||||
{
|
{
|
||||||
NoUserFound = 10,
|
NoUserFound = 10,
|
||||||
|
NoUser = 15,
|
||||||
UserExists = 20,
|
UserExists = 20,
|
||||||
PasswordIncorrect = 30,
|
PasswordIncorrect = 30,
|
||||||
UserLengthIncorrect = 33,
|
UserLengthIncorrect = 33,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Azaion.Common.Configs;
|
||||||
|
|
||||||
|
public class Constants
|
||||||
|
{
|
||||||
|
public const string HARDWARE_ID = nameof(HARDWARE_ID);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Azaion.Common.Configs;
|
||||||
|
|
||||||
|
public class JwtConfig
|
||||||
|
{
|
||||||
|
public string Issuer { get; set; } = null!;
|
||||||
|
public string Audience { get; set; } = null!;
|
||||||
|
public string Secret { get; set; }
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
namespace Azaion.Common;
|
namespace Azaion.Common.Entities;
|
||||||
|
|
||||||
public enum RoleEnum
|
public enum RoleEnum
|
||||||
{
|
{
|
||||||
Operator,
|
Operator,
|
||||||
Validator,
|
Validator,
|
||||||
CompanionPC,
|
CompanionPC,
|
||||||
Admin
|
Admin,
|
||||||
|
ApiAdmin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = null!;
|
public string Id { get; set; } = null!;
|
||||||
public string Username { 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!;
|
||||||
public RoleEnum Role { get; set; }
|
public RoleEnum Role { get; set; }
|
||||||
|
|
||||||
public string UniqueKey => $"Azaion#{Username}#{PasswordHash}#{HardwareId}";
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ namespace Azaion.Common.Requests;
|
|||||||
|
|
||||||
public class GetResourceRequest
|
public class GetResourceRequest
|
||||||
{
|
{
|
||||||
public string Username { get; set; } = null!;
|
|
||||||
public string Password { get; set; } = null!;
|
public string Password { get; set; } = null!;
|
||||||
public string HardwareId { get; set; } = null!;
|
public string HardwareId { get; set; } = null!;
|
||||||
public ResourceEnum ResourceEnum { get; set; }
|
public ResourceEnum ResourceEnum { get; set; }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Azaion.Common.Entities;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
|
||||||
namespace Azaion.Common.Requests;
|
namespace Azaion.Common.Requests;
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Http.Abstractions">
|
||||||
|
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.AspNetCore.Http.Abstractions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Microsoft.Extensions.Options">
|
<Reference Include="Microsoft.Extensions.Options">
|
||||||
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll</HintPath>
|
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|||||||
@@ -8,16 +8,15 @@ namespace Azaion.Services;
|
|||||||
|
|
||||||
public interface IResourcesService
|
public interface IResourcesService
|
||||||
{
|
{
|
||||||
Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default);
|
Task GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default);
|
||||||
Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default);
|
Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ResourcesService(IOptions<ResourcesConfig> resourcesConfig) : IResourcesService
|
public class ResourcesService(IOptions<ResourcesConfig> resourcesConfig) : IResourcesService
|
||||||
{
|
{
|
||||||
public async Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default)
|
public async Task GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var fileStream = new FileStream(GetResourcePath(request.ResourceEnum), FileMode.Open, FileAccess.Read);
|
var fileStream = new FileStream(GetResourcePath(resource), FileMode.Open, FileAccess.Read);
|
||||||
var key = Security.MakeEncryptionKey(request.Username, request.Password);
|
|
||||||
await fileStream.EncryptTo(outputStream, key, cancellationToken);
|
await fileStream.EncryptTo(outputStream, key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,80 @@
|
|||||||
using Azaion.Common;
|
using System.Security.Claims;
|
||||||
|
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(GetResourceRequest request, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserService(IDbFactory dbFactory) : IUserService
|
public class UserService(IDbFactory dbFactory, IHttpContextAccessor httpContextAccessor) : 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.Run(async db =>
|
||||||
{
|
{
|
||||||
var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Username == request.Email, token: cancellationToken);
|
var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Email == request.Email, token: cancellationToken);
|
||||||
if (existingUser != null)
|
if (existingUser != null)
|
||||||
throw new BusinessException(ExceptionEnum.UserExists, "User already exists");
|
throw new BusinessException(ExceptionEnum.UserExists, "User already exists");
|
||||||
|
|
||||||
await db.InsertAsync(new User
|
await db.InsertAsync(new User
|
||||||
{
|
{
|
||||||
Username = request.Email,
|
Email = request.Email,
|
||||||
PasswordHash = request.Password.ToHash(),
|
PasswordHash = request.Password.ToHash(),
|
||||||
Role = request.Role
|
Role = request.Role
|
||||||
}, token: cancellationToken);
|
}, token: cancellationToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User?> ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default) =>
|
public async Task<User> ValidateUser(string username, string password, string? hardwareId = null, CancellationToken cancellationToken = default) =>
|
||||||
await dbFactory.Run(async db =>
|
await dbFactory.Run(async db =>
|
||||||
{
|
{
|
||||||
var user = await db.Users.FirstOrDefaultAsync(x => x.Username == request.Username, token: cancellationToken);
|
var user = await db.Users.FirstOrDefaultAsync(x => x.Email == username, token: cancellationToken);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new BusinessException(ExceptionEnum.NoUserFound, "No user found");
|
throw new BusinessException(ExceptionEnum.NoUserFound, "No user found");
|
||||||
|
|
||||||
if (request.Password.ToHash() != user.PasswordHash)
|
if (password.ToHash() != user.PasswordHash)
|
||||||
throw new BusinessException(ExceptionEnum.PasswordIncorrect, "Passwords do not match");
|
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 (user.Role == RoleEnum.ApiAdmin)
|
||||||
if (string.IsNullOrEmpty(user.HardwareId))
|
return user;
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// For Non-API admins hardwareId should match if it was already set
|
||||||
|
if (user.HardwareId != null && user.HardwareId != hardwareId)
|
||||||
|
throw new BusinessException(ExceptionEnum.HardwareIdMismatch, "Hardware id mismatch");
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public async Task UpdateHardwareId(string username, string hardwareId, CancellationToken cancellationToken = default) =>
|
||||||
|
await dbFactory.Run(async db =>
|
||||||
|
await db.Users.UpdateAsync(x => x.Email == username, u => new User { HardwareId = hardwareId}, token: cancellationToken));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user