first commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*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
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
# Ignore Visual Studio-specific files
|
||||||
|
.vs/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Ignore Rider-specific files
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Ignore build output
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
**/out/
|
||||||
|
**/Debug/
|
||||||
|
**/Release/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Ignore user-specific files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Ignore .NET dependencies
|
||||||
|
**/project.lock.json
|
||||||
|
**/project.assets.json
|
||||||
|
**/node_modules/
|
||||||
|
**/packages/
|
||||||
|
**/test_results/
|
||||||
|
|
||||||
|
# Ignore sensitive configuration files
|
||||||
|
appsettings.json
|
||||||
|
appsettings.Development.json
|
||||||
|
secrets.json
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Ignore Docker-related files
|
||||||
|
docker-compose.override.yml
|
||||||
|
docker-compose.override.yaml
|
||||||
|
|
||||||
|
# Ignore certificates and private keys
|
||||||
|
*.pfx
|
||||||
|
*.p12
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
**/private_key.pem
|
||||||
|
**/private_key_pkcs8.pem
|
||||||
|
**/localhost.crt
|
||||||
|
**/localhost.key
|
||||||
|
**/localhost.pem
|
||||||
|
|
||||||
|
# Ignore logs and caches
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
**/*.pid
|
||||||
|
**/*.seed
|
||||||
|
|
||||||
|
# Ignore compiled files
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.pdb
|
||||||
|
*.cache
|
||||||
|
|
||||||
|
# Ignore Swagger-related files
|
||||||
|
swagger/
|
||||||
|
swagger-ui/
|
||||||
|
|
||||||
|
# Ignore database migrations and generated files
|
||||||
|
Migrations/
|
||||||
|
*.db
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# Ignore IDE-specific settings
|
||||||
|
*.Rider*
|
||||||
|
*.idea/
|
||||||
|
*.vs/
|
||||||
|
|
||||||
|
# Ignore test results
|
||||||
|
TestResults/
|
||||||
|
coverage/
|
||||||
|
coverage.xml
|
||||||
|
lcov-report/
|
||||||
|
|
||||||
|
# Ignore Kubernetes-related files
|
||||||
|
k8s/
|
||||||
|
|
||||||
|
# Ignore temporary files
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.old
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Auth;
|
||||||
|
|
||||||
|
public class LogOutMessageType : IMessageType
|
||||||
|
{
|
||||||
|
public List<string?>? Tokens { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string EventType { get; init; } = "Logout";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => JsonSerializer.Serialize(this);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<LogOutMessageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
Tokens = obj.Tokens;
|
||||||
|
UserName = obj.UserName;
|
||||||
|
Id = obj.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Kafka.Lib.Auth.Model;
|
||||||
|
|
||||||
|
public class ClaimChagesModel
|
||||||
|
{
|
||||||
|
public ClaimModel? Old { get; set; }
|
||||||
|
public ClaimModel? New { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Kafka.Lib.Auth.Model;
|
||||||
|
|
||||||
|
public class ClaimModel
|
||||||
|
{
|
||||||
|
public string? ClaimType { get; set; }
|
||||||
|
public string? ClaimValue { get; set; }
|
||||||
|
public string? ClaimIssuer { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Auth.Model
|
||||||
|
{
|
||||||
|
public class PrivilageModel
|
||||||
|
{
|
||||||
|
public string? Id { get; set; }
|
||||||
|
public string? PrivilageName { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => JsonSerializer.Serialize(this);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<PrivilageModel>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
Id = obj.Id;
|
||||||
|
PrivilageName = obj.PrivilageName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Kafka.Lib.Auth.Model;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Auth;
|
||||||
|
|
||||||
|
public class PermisionChangeMassageType : IMessageType
|
||||||
|
{
|
||||||
|
public PrivilageModel? PrivilageModel { get; set; }
|
||||||
|
public Dictionary<string, string> Changes { get; set; } = new();
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string? EventType { get; } = "PermisionChange";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(this);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<PermisionChangeMassageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
PrivilageModel = obj.PrivilageModel;
|
||||||
|
Changes = obj.Changes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Kafka.Lib.Auth.Model;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Auth;
|
||||||
|
|
||||||
|
public class RegisterNewServiceMessageType : IMessageType
|
||||||
|
{
|
||||||
|
public string? ServiceName { get; set; }
|
||||||
|
public string? ServiceAccountUserName { get; set; }
|
||||||
|
public List<string>? ServicePrivilage { get; set; }
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string? EventType { get; } = "RegisterNewService";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(this);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<RegisterNewServiceMessageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
ServiceName = obj.ServiceName;
|
||||||
|
ServiceAccountUserName = obj.ServiceAccountUserName;
|
||||||
|
ServicePrivilage = obj.ServicePrivilage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Auth;
|
||||||
|
|
||||||
|
public class TokenValidationMessageType : IMessageType
|
||||||
|
{
|
||||||
|
public string? Token { get; set; }
|
||||||
|
public string? Mess { get; set; }
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string? EventType { get; } = "TokenNotValid";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => JsonSerializer.Serialize(this);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<TokenValidationMessageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
Token = obj.Token;
|
||||||
|
Mess = obj.Mess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Kafka.Lib.Auth.Model;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Auth;
|
||||||
|
|
||||||
|
public class UserChangeClaimsMessageType : IMessageType
|
||||||
|
{
|
||||||
|
private string? _userId;
|
||||||
|
private List<ClaimChagesModel>? _claimChages;
|
||||||
|
private string? _id;
|
||||||
|
private bool _isSuccess = false;
|
||||||
|
private Dictionary<string,string> _errors = new ();
|
||||||
|
|
||||||
|
public string? UserId
|
||||||
|
{
|
||||||
|
get => _userId;
|
||||||
|
set => _userId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClaimChagesModel>? ClaimChages
|
||||||
|
{
|
||||||
|
get => _claimChages;
|
||||||
|
set => _claimChages = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
set => _id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? EventType { get; } = "UserChangeClaims";
|
||||||
|
|
||||||
|
public bool IsSuccess
|
||||||
|
{
|
||||||
|
get => _isSuccess;
|
||||||
|
set => _isSuccess = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> Errors
|
||||||
|
{
|
||||||
|
get => _errors;
|
||||||
|
set => _errors = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => JsonSerializer.Serialize(this);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<UserChangeClaimsMessageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
UserId = obj.UserId;
|
||||||
|
ClaimChages = obj.ClaimChages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Auth;
|
||||||
|
|
||||||
|
public class UserChangeMessageType : IMessageType
|
||||||
|
{
|
||||||
|
private string? _userId;
|
||||||
|
private Dictionary<string, string> _changes = new Dictionary<string, string>();
|
||||||
|
private string? _id;
|
||||||
|
private readonly string _eventType = "NewUser";
|
||||||
|
|
||||||
|
public string? UserId
|
||||||
|
{
|
||||||
|
get => _userId;
|
||||||
|
set => _userId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> Changes
|
||||||
|
{
|
||||||
|
get => _changes;
|
||||||
|
set => _changes = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
set => _id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EventType => _eventType;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => JsonSerializer.Serialize(this);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var val = JsonSerializer.Deserialize<UserChangeMessageType>(value);
|
||||||
|
if (val == null) throw new MessageNullException();
|
||||||
|
|
||||||
|
_changes = val.Changes;
|
||||||
|
_userId = val.UserId;
|
||||||
|
_id = val.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Kafka.Lib;
|
||||||
|
|
||||||
|
public interface IMessageType
|
||||||
|
{
|
||||||
|
public string? Id { get; set; }
|
||||||
|
public string? EventType { get; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Text { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Confluent.Kafka" Version="2.6.0" />
|
||||||
|
<PackageReference Include="Confluent.SchemaRegistry" Version="2.6.0" />
|
||||||
|
<PackageReference Include="MassTransit.Kafka" Version="8.3.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Kafka.Lib;
|
||||||
|
|
||||||
|
public class KafkaConfig
|
||||||
|
{
|
||||||
|
public string? BootstrapServers { get; set; }
|
||||||
|
public string? TopicRoot { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Unity;
|
||||||
|
|
||||||
|
public class MessageTypeSerializer<T> : ISerializer<T>
|
||||||
|
where T : IMessageType
|
||||||
|
{
|
||||||
|
public byte[] Serialize(T data, SerializationContext context)
|
||||||
|
{
|
||||||
|
var returnByte = JsonSerializer.SerializeToUtf8Bytes(data);
|
||||||
|
|
||||||
|
return returnByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Kafka.Lib.Warehouse.Model;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Warehouse;
|
||||||
|
|
||||||
|
public class InventoryChangeMessageType : IMessageType
|
||||||
|
{
|
||||||
|
public int ChangeRecordCount { get; set; }
|
||||||
|
public List<InventoryChangeModel> InventoryChanges { get; set; } = new();
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string? EventType { get; } = "InventoryChange";
|
||||||
|
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => JsonSerializer.Serialize(this);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<InventoryChangeMessageType>(value);
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
InventoryChanges = obj.InventoryChanges;
|
||||||
|
ChangeRecordCount = obj.ChangeRecordCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Kafka.Lib.Auth.Model;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Warehouse;
|
||||||
|
|
||||||
|
public class ItemClaimChangeMessageType : IMessageType
|
||||||
|
{
|
||||||
|
public string? ItemId { get; set; }
|
||||||
|
public List<ClaimChagesModel>? Chages { get; set; }
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string? EventType { get; } = "ItemClaimChange";
|
||||||
|
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(this);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<ItemClaimChangeMessageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
ItemId = obj.ItemId;
|
||||||
|
Chages = obj.Chages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Kafka.Lib.Warehouse.Model;
|
||||||
|
|
||||||
|
public class InventoryChangeModel
|
||||||
|
{
|
||||||
|
public InventoryChangeModel? Old { get; set; }
|
||||||
|
public InventoryChangeModel? New { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Kafka.Lib.Warehouse.Model;
|
||||||
|
|
||||||
|
public class InventoryModel
|
||||||
|
{
|
||||||
|
public string? ItemId { get; set; }
|
||||||
|
public string? InventoryId { get; set; }
|
||||||
|
public string? Position { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Warehouse;
|
||||||
|
|
||||||
|
public class ProducerChangeMessageType : IMessageType
|
||||||
|
{
|
||||||
|
public string? ProducerId { get; set; }
|
||||||
|
public Dictionary<string, string> Changes { get; set; } = new();
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string? EventType { get; } = "ProducerChange";
|
||||||
|
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(this);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<ProducerChangeMessageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
ProducerId = obj.ProducerId;
|
||||||
|
Changes = obj.Changes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Kafka.Lib.Warehouse;
|
||||||
|
|
||||||
|
public class WarehouseChangeMessageType : IMessageType
|
||||||
|
{
|
||||||
|
public string? WarehouseId { get; set; }
|
||||||
|
public Dictionary<string, string> Changes { get; set; } = new();
|
||||||
|
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string? EventType { get; } = "WarehouseChange";
|
||||||
|
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => JsonSerializer.Serialize(this);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<WarehouseChangeMessageType>(value);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
WarehouseId = obj.WarehouseId;
|
||||||
|
Changes = obj.Changes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using OpenWarehouse.auth.Common.ApiInterfeiceLib;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
using OpenWarehouse.auth.lib.Model.WellKnow;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.Common.Lib;
|
||||||
|
|
||||||
|
public static class AddAuthServices
|
||||||
|
{
|
||||||
|
public static async Task<T> AddAuthService<T>(this T builder, string authUrl, ILogger logger)
|
||||||
|
where T : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
var jwks = await builder.GetKey(authUrl, logger);
|
||||||
|
var conf = await builder.GetConfig(authUrl, logger);
|
||||||
|
|
||||||
|
var jwk = jwks.Keys[0];
|
||||||
|
|
||||||
|
var rsaParameters = new RSAParameters
|
||||||
|
{
|
||||||
|
Modulus = Base64UrlEncoder.DecodeBytes(jwk.N),
|
||||||
|
Exponent = Base64UrlEncoder.DecodeBytes(jwk.E)
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.Services.AddSingleton(_ =>
|
||||||
|
{
|
||||||
|
var rsa = RSA.Create();
|
||||||
|
rsa.ImportParameters(rsaParameters);
|
||||||
|
return rsa;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<RsaSecurityKey>(provider =>
|
||||||
|
{
|
||||||
|
var rsa = provider.GetRequiredService<RSA>();
|
||||||
|
return new RsaSecurityKey(rsa) { KeyId = jwk.Kid };
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
var rsaSecurityKey = builder.Services.BuildServiceProvider().GetRequiredService<RsaSecurityKey>();
|
||||||
|
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = rsaSecurityKey,
|
||||||
|
TokenDecryptionKey =
|
||||||
|
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:AesKey"] ?? "")),
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = conf.Issuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = builder.Configuration["Jwt:Audience"],
|
||||||
|
ValidateLifetime = true,
|
||||||
|
SaveSigninToken = true
|
||||||
|
};
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnTokenValidated = async context =>
|
||||||
|
{
|
||||||
|
var client = new OpenWarehouseAuthClient(authUrl, new HttpClient());
|
||||||
|
context.Request.Headers.TryGetValue("Authorization", out var token);
|
||||||
|
var result = await client.TokenIsLockAsync(new TokenAskIfIsLockModel { Token = token });
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
context.Fail("This token has been revoked.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OnAuthenticationFailed = context =>
|
||||||
|
{
|
||||||
|
logger.LogError("Authentication failed: {Exception}", context.Exception);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static async Task<OpenIdConfiguration> GetConfig<T>(this T builder, string url, ILogger logger)
|
||||||
|
where T : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Requesting OICD from: {Url}", url);
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
var client = new OpenWarehouseAuthClient(url, httpClient);
|
||||||
|
|
||||||
|
var result = await client.OpenidConfigurationAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("Successfully retrieved OICD from: {Url}", url);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to retrieve OICD from: {Url}", url);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<JwksResponse> GetKey<T>(this T builder, string url, ILogger logger)
|
||||||
|
where T : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Requesting JWKS from: {Url}", url);
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
var client = new OpenWarehouseAuthClient(url, httpClient);
|
||||||
|
|
||||||
|
var result = await client.JwksAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("Successfully retrieved JWKS from: {Url}", url);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to retrieve JWKS from: {Url}", url);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.Common.Lib;
|
||||||
|
|
||||||
|
public static class BuildLogger
|
||||||
|
{
|
||||||
|
public static ILogger GetBuildLogger<T>(this T builder)
|
||||||
|
where T : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder
|
||||||
|
.SetMinimumLevel(LogLevel.Information)
|
||||||
|
.AddConsole());
|
||||||
|
|
||||||
|
ILogger logger = loggerFactory.CreateLogger<T>();
|
||||||
|
|
||||||
|
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="8.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
|
||||||
|
<HintPath>..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.10\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenWarehouse.auth.lib\OpenWarehouse.auth.lib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OpenWarehouse.auth.Common.ApiInterfeiceLib;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.lib;
|
||||||
|
|
||||||
|
public static class Waiter
|
||||||
|
{
|
||||||
|
public static async Task<T> WaitForAut<T>(this T build, string? url, ILogger? logger)
|
||||||
|
where T : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
|
||||||
|
if (logger == null)
|
||||||
|
throw new NullReferenceException();
|
||||||
|
|
||||||
|
var client = new OpenWarehouseAuthClient(url, new HttpClient());
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var result = await client.PingAsync();
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
$"Auth connected with Ping: {(DateTime.FromBinary(result.Time) - now).TotalMilliseconds}");
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (i < 10)
|
||||||
|
{
|
||||||
|
++i;
|
||||||
|
logger.LogDebug($"Try {i} connect to auth fail.");
|
||||||
|
Thread.Sleep(10000);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogError($"Problem with auth service. \n " +
|
||||||
|
$"Mess: {e.Message}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Kafka.Lib.Auth;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
using OpenWarehouse.auth.api.Common.Exemptions;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
|
||||||
|
public class AuthTokenRepo(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ApplicationDbContext context,
|
||||||
|
ILogger<AuthTokenRepo> logger,
|
||||||
|
ITopicProducer<LogOutMessageType> logOutTopic,
|
||||||
|
ITopicProducer<TokenValidationMessageType> tokenValidationTopic,
|
||||||
|
IConfiguration configuration
|
||||||
|
) : IAuthTokenRepo
|
||||||
|
{
|
||||||
|
public async Task<bool> IsTokenLockAsync(string? token)
|
||||||
|
{
|
||||||
|
if (token.Contains("Bearer")) token = token.Split(" ")[1];
|
||||||
|
|
||||||
|
|
||||||
|
if (token == null) return true;
|
||||||
|
|
||||||
|
var tokenObj = ParseToken(token);
|
||||||
|
|
||||||
|
logger.LogInformation("Checking if token with GUID: {guid}, and user id {id} is locked", tokenObj?.Id, tokenObj?.Subject);
|
||||||
|
|
||||||
|
ApplicationUserTokenRepository? tokenM = await context.ApplicationUserTokenRepositories
|
||||||
|
.Include(q => q.Creator)
|
||||||
|
.Where(q => tokenObj != null && q.GuidToken != null && q.GuidToken.Equals(tokenObj.Id)).FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (tokenM?.IsLocked ?? true)
|
||||||
|
{
|
||||||
|
var tokenTopic = new TokenValidationMessageType()
|
||||||
|
{
|
||||||
|
Id = tokenM?.Id.ToString(),
|
||||||
|
Mess = "Token is lock",
|
||||||
|
Token = token
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = tokenValidationTopic.Produce(tokenTopic);
|
||||||
|
|
||||||
|
logger.LogDebug(tokenTopic.Text);
|
||||||
|
logger.LogWarning("Token is locked: {Token}", token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Token with GUID: {guid}, and user id {id} is unlocked", tokenObj?.Id, tokenObj?.Subject);
|
||||||
|
|
||||||
|
return tokenM?.IsLocked ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTokenLock(string? token)
|
||||||
|
{
|
||||||
|
var tokenObj = ParseToken(token);
|
||||||
|
|
||||||
|
logger.LogInformation("Checking if token with GUID: {guid}, and user id {id} is locked", tokenObj?.Id, tokenObj?.Subject);
|
||||||
|
|
||||||
|
|
||||||
|
ApplicationUserTokenRepository? tokenM = context.ApplicationUserTokenRepositories
|
||||||
|
.Include(q=>q.Creator)
|
||||||
|
.FirstOrDefault(q => q.GuidToken != null && q.GuidToken.Equals(token));
|
||||||
|
|
||||||
|
|
||||||
|
if (tokenM?.IsLocked ?? true)
|
||||||
|
{
|
||||||
|
var tokenTopic = new TokenValidationMessageType()
|
||||||
|
{
|
||||||
|
Id = tokenM?.Id.ToString(),
|
||||||
|
Mess = "Token is lock",
|
||||||
|
Token = token
|
||||||
|
};
|
||||||
|
|
||||||
|
tokenValidationTopic.Produce(tokenTopic);
|
||||||
|
logger.LogDebug(tokenTopic.Text);
|
||||||
|
logger.LogWarning("Token is locked: {Token}", token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logger.LogInformation("Token with GUID: {guid}, and user id {id} is unlocked", tokenObj?.Id, tokenObj?.Subject);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> LockTokenAsync(string? token)
|
||||||
|
{
|
||||||
|
var tokenObj = ParseToken(token);
|
||||||
|
|
||||||
|
logger.LogInformation("Running LockTokenAsync function for GUID: {Guid}, User ID: {UserId}", tokenObj?.Id, tokenObj?.Subject);
|
||||||
|
|
||||||
|
ApplicationUserTokenRepository? tokenM = await context.ApplicationUserTokenRepositories
|
||||||
|
.Where(q => tokenObj != null && q.GuidToken != null && q.GuidToken.Equals(tokenObj.Id))
|
||||||
|
.Include(q => q.Creator).FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (tokenM != null)
|
||||||
|
{
|
||||||
|
if (tokenM.IsLocked)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Token is already locked for GUID: {Guid}, User ID: {UserId}", tokenObj?.Id, tokenObj?.Subject);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
tokenM.IsLocked = true;
|
||||||
|
if (tokenM.Creator == null) throw new AccountExemptions(false, "Account not found!");
|
||||||
|
|
||||||
|
var topic = new LogOutMessageType()
|
||||||
|
{
|
||||||
|
Id = tokenM.Id.ToString(),
|
||||||
|
Tokens = [token],
|
||||||
|
UserName = tokenM.Creator.UserName
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = logOutTopic.Produce(topic);
|
||||||
|
logger.LogDebug("Produced logout topic for token: {Token}", topic.Text);
|
||||||
|
logger.LogDebug("Token locked and logout topic produced for GUID: {Guid}, User ID: {UserId}", tokenObj?.Id, tokenObj?.Subject);
|
||||||
|
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> LockAllUserTokenAsync(ApplicationUser user)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Locking all tokens for user: {UserId}", user.Id);
|
||||||
|
|
||||||
|
var topic = new LogOutMessageType()
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
UserName = user.UserName
|
||||||
|
};
|
||||||
|
int count = 0;
|
||||||
|
foreach (var tokenRepository in context.ApplicationUserTokenRepositories.Where(repository =>
|
||||||
|
repository.Creator == user && !repository.IsLocked))
|
||||||
|
{
|
||||||
|
if (tokenRepository.GuidToken != null) topic.Tokens?.Add(tokenRepository.GuidToken);
|
||||||
|
tokenRepository.IsLocked = true;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
}
|
||||||
|
logger.LogDebug("Produced logout topic for user: {UserId}, Tokens: {Tokens}", user.Id, topic.Tokens);
|
||||||
|
_ = logOutTopic.Produce(topic);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
logger.LogInformation("Locked {Count} tokens for user: {UserId}", count, user.Id);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int LockToken(string token)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Locking token: {Token}", token);
|
||||||
|
ApplicationUserTokenRepository? tokenM =
|
||||||
|
context.ApplicationUserTokenRepositories.FirstOrDefault(q => q.GuidToken != null && q.GuidToken.Equals(token));
|
||||||
|
if (tokenM != null)
|
||||||
|
{
|
||||||
|
tokenM.IsLocked = true;
|
||||||
|
logger.LogInformation("Token locked successfully: {Token}", token);
|
||||||
|
return context.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("Token not found for locking: {Token}", token);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> AddTokenToRepAsync(string guid, string creator, DateTime exp, DateTime? created = null)
|
||||||
|
{
|
||||||
|
ApplicationUser? user = await userManager.FindByEmailAsync(creator) ??
|
||||||
|
await userManager.FindByNameAsync(creator);
|
||||||
|
|
||||||
|
logger.LogInformation("Adding token to repository for creator: {Creator}", creator);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
return await AddTokenToRepAsync(guid, user, exp,created);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("User not found for token creation: {Creator}", creator);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<int> AddTokenToRepAsync(string guid, ApplicationUser? creator, DateTime? exp, DateTime? created = null)
|
||||||
|
{
|
||||||
|
created ??= DateTime.UtcNow;
|
||||||
|
if (creator == null) return 0;
|
||||||
|
ApplicationUserTokenRepository newToken = new ApplicationUserTokenRepository(guid, creator, created, exp);
|
||||||
|
context.ApplicationUserTokenRepositories.Add(newToken);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> DeleteAllUserTokenAsync(ApplicationUser user)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Deleting all tokens for user: {UserId}", user.Id);
|
||||||
|
var tokens = await context.ApplicationUserTokenRepositories
|
||||||
|
.Where(q => q.Creator == user)
|
||||||
|
.Select(q => q.GuidToken)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var tokensInt = await context.ApplicationUserTokenRepositories
|
||||||
|
.Where(q => q.Creator == user).ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
var topic = new LogOutMessageType()
|
||||||
|
{
|
||||||
|
UserName = user.UserName ?? "User not found",
|
||||||
|
Id = user.Id,
|
||||||
|
Tokens = tokens
|
||||||
|
};
|
||||||
|
await logOutTopic.Produce(topic);
|
||||||
|
logger.LogInformation("Deleted {Count} tokens for user: {UserId}", tokensInt, user.Id);
|
||||||
|
return tokensInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetLastUserToken(ApplicationUser? user)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Retrieving last token for user: {UserId}", user?.Id);
|
||||||
|
return await context.ApplicationUserTokenRepositories.Where(repository =>
|
||||||
|
repository.Creator == user && repository.ExpireTime < DateTime.UtcNow).Select(repository => repository.GuidToken).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> DeleteInactiveToken()
|
||||||
|
{
|
||||||
|
logger.LogInformation("Deleting inactive tokens");
|
||||||
|
var deletedToken =
|
||||||
|
await context.ApplicationUserTokenRepositories.Where(repository =>
|
||||||
|
repository.ExpireTime < DateTime.UtcNow.AddHours(-2)).ExecuteDeleteAsync();
|
||||||
|
logger.LogInformation("Deleted {Count} inactive tokens", deletedToken);
|
||||||
|
return deletedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> CreateJwtToken(ApplicationUser user, DateTime? expires, List<Claim>? claims)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Generating JWT token for user: {UserId}", user.Id);
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
|
||||||
|
var rsaSecurityKey = await GetSigningCredentials();
|
||||||
|
|
||||||
|
var audience = configuration["Jwt:Audience"] ?? string.Empty;
|
||||||
|
var issuer = configuration["Jwt:Issuer"] ?? string.Empty;
|
||||||
|
|
||||||
|
if (user is { UserName: not null, Email: not null })
|
||||||
|
{
|
||||||
|
var claimList = await userManager.GetClaimsAsync(user);
|
||||||
|
var claim = claimList.FirstOrDefault(claim1 => claim1.Type == AccountTypeFunction.AccountTypeType);
|
||||||
|
var guid = Guid.NewGuid().ToString();
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtRegisteredClaimNames.Jti, guid),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||||
|
new Claim(ClaimTypes.Name, user.UserName),
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
claim ?? AccountType.Uknow.Claim(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Expires = expires,
|
||||||
|
Audience = audience,
|
||||||
|
Issuer = issuer,
|
||||||
|
SigningCredentials = rsaSecurityKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
string? tokenString = tokenHandler.WriteToken(token);
|
||||||
|
await AddTokenToRepAsync(guid, user, DateTime.UtcNow, tokenDescriptor.Expires);
|
||||||
|
logger.LogInformation("JWT token generated successfully for user: {UserId}", user.Id);
|
||||||
|
return tokenString;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("Failed to generate JWT token due to missing user information.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> RegenerateJwtToken(string? token, DateTime? expires)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var tokenObj = tokenHandler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
var claims = tokenObj.Claims.ToList();
|
||||||
|
|
||||||
|
claims.RemoveAll(c => c.Type == JwtRegisteredClaimNames.Jti ||
|
||||||
|
c.Type == JwtRegisteredClaimNames.Iat ||
|
||||||
|
c.Type == JwtRegisteredClaimNames.Nbf ||
|
||||||
|
c.Type == "unique_name" ||
|
||||||
|
c.Type == "email");
|
||||||
|
|
||||||
|
var newJti = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
claims.Add(new Claim(JwtRegisteredClaimNames.Jti, newJti));
|
||||||
|
|
||||||
|
var rsaSecurityKey = await GetSigningCredentials();
|
||||||
|
|
||||||
|
var userId = claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
logger.LogWarning("Cannot regenerate token: user ID not found in token claims.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Cannot regenerate token: user not found.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(user.UserName) && !string.IsNullOrEmpty(user.Email))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
|
||||||
|
claims.Add(new Claim(ClaimTypes.Email, user.Email));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LockTokenAsync(token);
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
Expires = expires,
|
||||||
|
SigningCredentials = rsaSecurityKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
var newToken = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
string? tokenString = tokenHandler.WriteToken(newToken);
|
||||||
|
await AddTokenToRepAsync(newJti, user, DateTime.UtcNow, tokenDescriptor.Expires);
|
||||||
|
|
||||||
|
return tokenString;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> TokenIsLock(string? token = null, string? guid = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (guid == null && token != null)
|
||||||
|
{
|
||||||
|
guid = ParseToken(token).Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (guid == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Token fot GUID: {guid} is invalid.", guid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var result = await context.ApplicationUserTokenRepositories
|
||||||
|
.Where(repository => repository.GuidToken != null && repository.GuidToken.Equals(guid))
|
||||||
|
.Select(repository => repository.IsLocked).FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JwtSecurityToken ParseToken(string? token)
|
||||||
|
{
|
||||||
|
logger.LogTrace("Parse token");
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var tokenObj = tokenHandler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
logger.LogDebug("Parsed token with GUID: {guid} and userID: {id}", tokenObj.Id, tokenObj.Subject);
|
||||||
|
|
||||||
|
return tokenObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<SigningCredentials> GetSigningCredentials()
|
||||||
|
{
|
||||||
|
var contentRoot = configuration["contentRoot"] ?? throw new NullReferenceException();
|
||||||
|
|
||||||
|
var privateKeyPath = Path.Combine(contentRoot, "private_key_pkcs8.pem");
|
||||||
|
|
||||||
|
var rsa = RSA.Create();
|
||||||
|
|
||||||
|
rsa.ImportFromPem(await File.ReadAllTextAsync(privateKeyPath));
|
||||||
|
|
||||||
|
var key = new RsaSecurityKey(rsa)
|
||||||
|
{
|
||||||
|
KeyId = "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SigningCredentials(key , SecurityAlgorithms.RsaSha256);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
|
||||||
|
public interface IAuthTokenRepo
|
||||||
|
{
|
||||||
|
public Task<bool> IsTokenLockAsync(string? token);
|
||||||
|
public bool IsTokenLock(string? token);
|
||||||
|
|
||||||
|
public Task<int> LockTokenAsync(string? token);
|
||||||
|
public Task<int> LockAllUserTokenAsync(ApplicationUser user);
|
||||||
|
public int LockToken(string token);
|
||||||
|
|
||||||
|
public Task<int> AddTokenToRepAsync(string token, string creator, DateTime exp, DateTime? created);
|
||||||
|
|
||||||
|
public Task<int> AddTokenToRepAsync(string token, ApplicationUser? creator, DateTime? exp, DateTime? created);
|
||||||
|
|
||||||
|
public Task<int> DeleteAllUserTokenAsync(ApplicationUser user);
|
||||||
|
|
||||||
|
public Task<string?> GetLastUserToken(ApplicationUser? user);
|
||||||
|
|
||||||
|
public Task<int> DeleteInactiveToken();
|
||||||
|
|
||||||
|
public Task<string?> CreateJwtToken(ApplicationUser user, DateTime? expires, List<Claim> claims);
|
||||||
|
|
||||||
|
public Task<string?> RegenerateJwtToken(string? token, DateTime? expires);
|
||||||
|
|
||||||
|
public Task<bool> TokenIsLock(string? token, string? guid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
|
||||||
|
public class UserTokenInformation
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
|
||||||
|
public enum AccountClaims
|
||||||
|
{
|
||||||
|
ServiceAdress, IfUserCanChangeRole, ServiceName,
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
|
||||||
|
public static class AccountClaimsClass
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<AccountClaims, string> AccountTypeDict = new()
|
||||||
|
{
|
||||||
|
{ AccountClaims.ServiceAdress, "Service_Adress" },
|
||||||
|
{ AccountClaims.ServiceName, "ServiceName" },
|
||||||
|
{ AccountClaims.IfUserCanChangeRole, "PrivilageFunct" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Claim GetClaim(this AccountClaims claims, string val)
|
||||||
|
{
|
||||||
|
var isSuccess = AccountTypeDict.TryGetValue(claims,out var res);
|
||||||
|
|
||||||
|
if (isSuccess && res != null)
|
||||||
|
{
|
||||||
|
return new Claim(res, val);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("Claim not implemented!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetName(AccountClaims claims)
|
||||||
|
{
|
||||||
|
var isSuccess = AccountTypeDict.TryGetValue(claims,out var res);
|
||||||
|
|
||||||
|
|
||||||
|
if (isSuccess && res != null)
|
||||||
|
{
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("Claim not implemented!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
|
||||||
|
public enum AccountType
|
||||||
|
{
|
||||||
|
User, Admin, Service, Uknow
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
|
||||||
|
public static class AccountTypeFunction
|
||||||
|
{
|
||||||
|
public static string AccountTypeType { get; } = "AccountType";
|
||||||
|
|
||||||
|
private static readonly Dictionary<AccountType, string> AccountTypeMap = new()
|
||||||
|
{
|
||||||
|
{ AccountType.User, "User" },
|
||||||
|
{ AccountType.Admin, "Admin" },
|
||||||
|
{ AccountType.Service, "Service" },
|
||||||
|
{ AccountType.Uknow, "Uknow" },
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetValue(this AccountType accountType)
|
||||||
|
{
|
||||||
|
var isSuccess = AccountTypeMap.TryGetValue(accountType, out var acountString);
|
||||||
|
|
||||||
|
if (isSuccess && acountString != null)
|
||||||
|
{
|
||||||
|
return acountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Account type not in AccountTypeMap.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Claim Claim(this AccountType accountType)
|
||||||
|
{
|
||||||
|
return new Claim(AccountTypeType, accountType.GetValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.DefaultUserAndRole;
|
||||||
|
|
||||||
|
public class DefaultIdentify(
|
||||||
|
ILogger<DefaultIdentify> logger,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
RoleManager<ApplicationRole> roleManager,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ApplicationDbContext applicationDbContext,
|
||||||
|
PrivilageManager.PrivilegeManager privilegeManager,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
public async Task SeedAsync()
|
||||||
|
{
|
||||||
|
if (!await applicationDbContext.RolePrivilages.AnyAsync(x => true))
|
||||||
|
{
|
||||||
|
var listOfRoles = GlobalPrivilage.GatherPrivilegeStrings();
|
||||||
|
foreach (var privilage in listOfRoles)
|
||||||
|
{
|
||||||
|
var rolePrivilage = new RolePrivilage() { Privilage = privilage };
|
||||||
|
applicationDbContext.RolePrivilages.Add(rolePrivilage);
|
||||||
|
logger.LogDebug($"Created privilage: {privilage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await applicationDbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!roleManager.Roles.Any())
|
||||||
|
{
|
||||||
|
ApplicationRole admin = new ApplicationRole()
|
||||||
|
{
|
||||||
|
Name = "Admin",
|
||||||
|
IsGlobalAdmin = true,
|
||||||
|
};
|
||||||
|
ApplicationRole user = new ApplicationRole()
|
||||||
|
{
|
||||||
|
Name = "User",
|
||||||
|
IsGlobalAdmin = false
|
||||||
|
};
|
||||||
|
ApplicationRole service = new ApplicationRole()
|
||||||
|
{
|
||||||
|
Name = "Service",
|
||||||
|
IsGlobalAdmin = true
|
||||||
|
};
|
||||||
|
if ((await roleManager.CreateAsync(admin)).Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogDebug($"Created role {admin}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Unable create role {admin}");
|
||||||
|
}
|
||||||
|
if ((await roleManager.CreateAsync(user)).Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogDebug($"Created role {user.Name}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Unable create role {user.Name}");
|
||||||
|
}
|
||||||
|
if ((await roleManager.CreateAsync(service)).Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogDebug($"Created role {service.Name}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Unable create role {service.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userManager.Users.Any())
|
||||||
|
{
|
||||||
|
var username = configuration["SuperUser:Name"] ?? "Admin";
|
||||||
|
var email = configuration["SuperUser:Email"] ?? "Admin@admin.local";
|
||||||
|
|
||||||
|
var password = configuration["SuperUser:Password"] ?? GenerateRandomPass(10);
|
||||||
|
|
||||||
|
|
||||||
|
var user = new ApplicationUser() { UserName = username, Email = email, EmailConfirmed = true};
|
||||||
|
var userResult = (await userManager.CreateAsync(user, password));
|
||||||
|
if (userResult.Succeeded)
|
||||||
|
{
|
||||||
|
var claimResult = await userManager.AddClaimAsync(user, AccountType.Admin.Claim());
|
||||||
|
|
||||||
|
if (claimResult.Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Sucess added claim {AccountType.Admin.Claim()} to user {user.UserName}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Unable added claim {AccountType.Admin.Claim()} to user {user.UserName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Created user, Name: {user.UserName} Email: {user.Email}, Password: {password}");
|
||||||
|
var roleResult = (await userManager.AddToRoleAsync(user, "Admin"));
|
||||||
|
if (roleResult.Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Added role Admin to {user.UserName}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Unable added role Admin to user {user.UserName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Unable create user {user.UserToken}. Messs: {string.Join(";",userResult.Errors)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateRandomPass(int length)
|
||||||
|
{
|
||||||
|
const string upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const string lower = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
const string digits = "0123456789";
|
||||||
|
const string specialChars = "!@#$%^&*()_+{}\":'<>?[];,./";
|
||||||
|
|
||||||
|
var allChars = upper + lower + digits + specialChars;
|
||||||
|
var random = new Random();
|
||||||
|
var passwordChars = new char[length];
|
||||||
|
|
||||||
|
passwordChars[0] = upper[random.Next(upper.Length)];
|
||||||
|
passwordChars[1] = lower[random.Next(lower.Length)];
|
||||||
|
passwordChars[2] = digits[random.Next(digits.Length)];
|
||||||
|
passwordChars[3] = specialChars[random.Next(specialChars.Length)];
|
||||||
|
|
||||||
|
for (int i = 4; i < length; i++)
|
||||||
|
{
|
||||||
|
passwordChars[i] = allChars[random.Next(allChars.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(passwordChars.OrderBy(c => random.Next()).ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.Exemptions;
|
||||||
|
|
||||||
|
public class AccountExemptions : Exception
|
||||||
|
{
|
||||||
|
private Dictionary<string, string> _otherProblem;
|
||||||
|
private string _message;
|
||||||
|
private bool _accountExist;
|
||||||
|
|
||||||
|
|
||||||
|
public bool AccountExist
|
||||||
|
{
|
||||||
|
get => _accountExist;
|
||||||
|
set => _accountExist = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Message
|
||||||
|
{
|
||||||
|
get => _message;
|
||||||
|
set => _message = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> OtherProblem
|
||||||
|
{
|
||||||
|
get => _otherProblem;
|
||||||
|
set => _otherProblem = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountExemptions(bool accountExist, string message, Dictionary<string, string>? otherProblem = null)
|
||||||
|
{
|
||||||
|
_otherProblem = otherProblem ?? new Dictionary<string, string>();
|
||||||
|
_accountExist = accountExist;
|
||||||
|
_message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.Exemptions;
|
||||||
|
|
||||||
|
public class RoleExemptions : Exception
|
||||||
|
{
|
||||||
|
private Dictionary<string, string> _otherProblem;
|
||||||
|
private string? _message;
|
||||||
|
private bool _roleExist;
|
||||||
|
|
||||||
|
|
||||||
|
public bool RoleExist
|
||||||
|
{
|
||||||
|
get => _roleExist;
|
||||||
|
set => _roleExist = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Message => _message ?? "";
|
||||||
|
|
||||||
|
public Dictionary<string, string> OtherProblem
|
||||||
|
{
|
||||||
|
get => _otherProblem;
|
||||||
|
set => _otherProblem = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoleExemptions(bool accountExist = true, string? message = null, Dictionary<string, string>? otherProblem = null)
|
||||||
|
{
|
||||||
|
_otherProblem = otherProblem ?? new Dictionary<string, string>();
|
||||||
|
_roleExist = accountExist;
|
||||||
|
_message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Kafka.Lib;
|
||||||
|
using MassTransit;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.Kafka;
|
||||||
|
|
||||||
|
public class Consumer : IConsumer<IMessageType>
|
||||||
|
{
|
||||||
|
public Task Consume(ConsumeContext<IMessageType> context)
|
||||||
|
{
|
||||||
|
Console.Out.WriteLine($"{context.Message.Text}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.PrivilageManager;
|
||||||
|
|
||||||
|
public static class GlobalPrivilage
|
||||||
|
{
|
||||||
|
public enum Privilege
|
||||||
|
{
|
||||||
|
GlobalAdmin,
|
||||||
|
GlobalManageUser,
|
||||||
|
GlobalCreateUser,
|
||||||
|
GlobalDeleteUser,
|
||||||
|
GlobalEditUser,
|
||||||
|
GlobalManageRole,
|
||||||
|
GlobalCreateRole,
|
||||||
|
GlobalDeleteRole,
|
||||||
|
GlobalEditRole,
|
||||||
|
GlobalDeleteExpiredToken,
|
||||||
|
GlobalDeactivateUserToken,
|
||||||
|
GlobalUser
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Dictionary<Privilege, string> PrivilegeToStringMap = new()
|
||||||
|
{
|
||||||
|
{ Privilege.GlobalAdmin, "Auth-Admin" },
|
||||||
|
{ Privilege.GlobalManageUser, "Auth-User-Manage" },
|
||||||
|
{ Privilege.GlobalCreateUser, "Auth-User-Create" },
|
||||||
|
{ Privilege.GlobalDeleteUser, "Auth-User-Delete" },
|
||||||
|
{ Privilege.GlobalEditUser, "Auth-User-Edit" },
|
||||||
|
{ Privilege.GlobalManageRole, "Auth-Role-Manage" },
|
||||||
|
{ Privilege.GlobalCreateRole, "Auth-Role-Create-" },
|
||||||
|
{ Privilege.GlobalDeleteRole, "Auth-Role-Delete" },
|
||||||
|
{ Privilege.GlobalEditRole, "Auth-Role-Edit" },
|
||||||
|
{ Privilege.GlobalDeleteExpiredToken, "Auth-Delete-Expired-Token" },
|
||||||
|
{ Privilege.GlobalDeactivateUserToken, "Auth-Deactivate-User-Token" },
|
||||||
|
{ Privilege.GlobalUser, "Auth-User" }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly Dictionary<Privilege, List<Privilege>> PrivilegeMap = new()
|
||||||
|
{
|
||||||
|
{ Privilege.GlobalAdmin, new List<Privilege> { Privilege.GlobalAdmin } },
|
||||||
|
{ Privilege.GlobalManageUser, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageUser } },
|
||||||
|
{ Privilege.GlobalCreateUser, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageUser, Privilege.GlobalCreateUser } },
|
||||||
|
{ Privilege.GlobalDeleteUser, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageUser, Privilege.GlobalDeleteUser } },
|
||||||
|
{ Privilege.GlobalEditUser, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageUser, Privilege.GlobalEditUser } },
|
||||||
|
{ Privilege.GlobalManageRole, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageRole } },
|
||||||
|
{ Privilege.GlobalCreateRole, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageRole, Privilege.GlobalCreateRole } },
|
||||||
|
{ Privilege.GlobalDeleteRole, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageRole, Privilege.GlobalDeleteRole } },
|
||||||
|
{ Privilege.GlobalEditRole, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalManageRole, Privilege.GlobalEditRole } },
|
||||||
|
{ Privilege.GlobalDeactivateUserToken, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalDeactivateUserToken } },
|
||||||
|
{ Privilege.GlobalDeleteExpiredToken, new List<Privilege> { Privilege.GlobalAdmin, Privilege.GlobalDeleteExpiredToken } },
|
||||||
|
{ Privilege.GlobalUser , GetAllPrivilages()}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public static List<string> GetPrivilegesFor(this Privilege privilegeKey)
|
||||||
|
{
|
||||||
|
var privilegeStrings = new List<string>();
|
||||||
|
|
||||||
|
if (PrivilegeMap.TryGetValue(privilegeKey, out var privileges))
|
||||||
|
{
|
||||||
|
foreach (var privilege in privileges)
|
||||||
|
{
|
||||||
|
if (PrivilegeToStringMap.TryGetValue(privilege, out var privilegeString))
|
||||||
|
{
|
||||||
|
privilegeStrings.Add(privilegeString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return privilegeStrings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetStringForPrivilege(this Privilege privilege)
|
||||||
|
{
|
||||||
|
|
||||||
|
bool isSuccess = PrivilegeToStringMap.TryGetValue(privilege, out var privilegeString);
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
return privilegeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<string> GatherPrivilegeStrings()
|
||||||
|
{
|
||||||
|
var privilegeStrings = new List<string>();
|
||||||
|
|
||||||
|
foreach (var privilege in GetAllPrivilages())
|
||||||
|
{
|
||||||
|
var privilegeString = privilege.GetStringForPrivilege();
|
||||||
|
if (privilegeString != null)
|
||||||
|
{
|
||||||
|
privilegeStrings.Add(privilegeString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return privilegeStrings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Privilege> GetAllPrivilages()
|
||||||
|
{
|
||||||
|
var allPrivileges = new List<Privilege>();
|
||||||
|
foreach (var privilege in Enum.GetValues(typeof(Privilege)))
|
||||||
|
{
|
||||||
|
allPrivileges.Add((Privilege)privilege);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPrivileges;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.PrivilageManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to enforce privilege-based authorization.
|
||||||
|
/// </summary>
|
||||||
|
public class HasPrivilegeAttribute : Attribute, IAsyncAuthorizationFilter
|
||||||
|
{
|
||||||
|
private GlobalPrivilage.Privilege[] Privileges { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HasPrivilegeAttribute"/> class with one or more required privileges.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="privileges">The privileges required to access the resource.</param>
|
||||||
|
public HasPrivilegeAttribute(params GlobalPrivilage.Privilege[] privileges)
|
||||||
|
{
|
||||||
|
Privileges = privileges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs the privilege check during authorization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The authorization filter context.</param>
|
||||||
|
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||||
|
{
|
||||||
|
var userActionId = context.HttpContext.User.FindFirstValue(JwtRegisteredClaimNames.Sub);
|
||||||
|
|
||||||
|
var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
var privilegeManager = context.HttpContext.RequestServices.GetRequiredService<PrivilegeManager>();
|
||||||
|
|
||||||
|
if (userActionId != null)
|
||||||
|
{
|
||||||
|
var userAction = await userManager.FindByIdAsync(userActionId);
|
||||||
|
if (userAction == null ||
|
||||||
|
!await UserHasAnyRequiredPrivilege(privilegeManager, userAction))
|
||||||
|
{
|
||||||
|
context.Result = new ContentResult
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status403Forbidden,
|
||||||
|
Content = "Access Denied: You do not have the required privileges to perform this action.",
|
||||||
|
ContentType = "text/plain"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the user has at least one of the required privileges.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="privilegeManager">The privilege manager service.</param>
|
||||||
|
/// <param name="user">The user to check privileges for.</param>
|
||||||
|
/// <returns>True if the user has any required privilege; otherwise, false.</returns>
|
||||||
|
private async Task<bool> UserHasAnyRequiredPrivilege(PrivilegeManager privilegeManager, ApplicationUser user)
|
||||||
|
{
|
||||||
|
foreach (var privilege in Privileges)
|
||||||
|
{
|
||||||
|
if (await privilegeManager.UserHasGlobalPrivilage(user, privilege))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using OpenWarehouse.auth.lib.Model.Service;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.PrivilageManager;
|
||||||
|
|
||||||
|
public interface IPrivilageManager
|
||||||
|
{
|
||||||
|
|
||||||
|
public Task<List<string?>> GetUserPrivilage(ApplicationUser user);
|
||||||
|
public Task<List<string>?> GetRolePrivilage(ApplicationRole role);
|
||||||
|
|
||||||
|
public Task AddRolePrivilage(ApplicationRole role, string privilage);
|
||||||
|
public Task DeleteRolePrivilage(ApplicationRole role, string privilage);
|
||||||
|
|
||||||
|
public Task<bool> RoleHasPrivilage(ApplicationRole role, string privilage);
|
||||||
|
public Task<bool> UserHasPrivilage(ApplicationUser user, string privilage);
|
||||||
|
public Task<bool> UserHasGlobalPrivilage(ApplicationUser user, GlobalPrivilage.Privilege privilege);
|
||||||
|
|
||||||
|
public Task<RolePrivilage?> GetPrivilageByNameIfDontExistCreateAsync(string privilage);
|
||||||
|
|
||||||
|
public Task<RolePrivilage?> GetPrivilageByName(string privilage);
|
||||||
|
|
||||||
|
public Task<CreateServiceRoleResult> CreatePrivageAsync(string privilage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using OpenWarehouse.auth.lib.Model.Service;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.PrivilageManager;
|
||||||
|
|
||||||
|
public class PrivilegeManager(ApplicationDbContext context,
|
||||||
|
RoleManager<ApplicationRole> roleManager,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<PrivilegeManager> logger) : IPrivilageManager
|
||||||
|
{
|
||||||
|
public async Task<List<string?>> GetUserPrivilage(ApplicationUser user)
|
||||||
|
{
|
||||||
|
var roles = await userManager.GetRolesAsync(user);
|
||||||
|
List<string?> privilages = [];
|
||||||
|
|
||||||
|
foreach (var role in roles)
|
||||||
|
{
|
||||||
|
var roleObj = await context.Roles.Include(applicationRole => applicationRole.Privilege).FirstOrDefaultAsync(applicationRole =>
|
||||||
|
applicationRole.NormalizedName.Equals(roleManager.NormalizeKey(role)));
|
||||||
|
|
||||||
|
if (roleObj != null && roleObj.IsGlobalAdmin)
|
||||||
|
privilages.Add("Admin");
|
||||||
|
|
||||||
|
if (roleObj is { Privilege: not null })
|
||||||
|
privilages.AddRange(roleObj.Privilege.Select(privilage => privilage?.Privilage).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return privilages.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>?> GetRolePrivilage(ApplicationRole role)
|
||||||
|
{
|
||||||
|
|
||||||
|
List<string>? privilageList = [];
|
||||||
|
if (role.IsGlobalAdmin)
|
||||||
|
{
|
||||||
|
privilageList.Add("Admin");
|
||||||
|
return privilageList;
|
||||||
|
}
|
||||||
|
|
||||||
|
var privilage = await context.RolePrivilages
|
||||||
|
.Where(rolePrivilage => rolePrivilage.Roles.Any(applicationRole => applicationRole == role)).ToListAsync();
|
||||||
|
|
||||||
|
if (privilage != null)
|
||||||
|
privilageList = privilage.Select(privilage => privilage.Privilage).Distinct().ToList();
|
||||||
|
|
||||||
|
return privilageList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddRolePrivilage(ApplicationRole role, string privilage)
|
||||||
|
{
|
||||||
|
var privilageObj = await context.RolePrivilages.Include(rolePrivilage => rolePrivilage.Roles)
|
||||||
|
.FirstOrDefaultAsync(rolePrivilage => rolePrivilage.Privilage.Equals(privilage));
|
||||||
|
|
||||||
|
if(privilageObj is { Roles: not null }
|
||||||
|
&& !privilageObj.Roles.Any(applicationRole => applicationRole.Id == role.Id))
|
||||||
|
privilageObj.Roles.Add(role);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteRolePrivilage(ApplicationRole role, string privilage)
|
||||||
|
{
|
||||||
|
var privilageToDelete = await context.RolePrivilages
|
||||||
|
.Where(rolePrivilage => rolePrivilage.Privilage == privilage &&
|
||||||
|
rolePrivilage.Roles.Any(applicationRole => applicationRole.Id == role.Id))
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (privilageToDelete != null)
|
||||||
|
{
|
||||||
|
context.RolePrivilages.Remove(privilageToDelete);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RoleHasPrivilage(ApplicationRole role, string privilage)
|
||||||
|
{
|
||||||
|
if (role.IsGlobalAdmin) return true;
|
||||||
|
|
||||||
|
if (role.Privilege == null) return false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return role.Privilege.Any(rolePrivilage => rolePrivilage.Privilage == privilage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasPrivilage(ApplicationUser user, string privilage)
|
||||||
|
{
|
||||||
|
var rolesName = await userManager.GetRolesAsync(user);
|
||||||
|
foreach (var roleName in rolesName)
|
||||||
|
{
|
||||||
|
var role = await roleManager.FindByNameAsync(roleName);
|
||||||
|
if (role == null) return false;
|
||||||
|
return await RoleHasPrivilage(role, privilage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasGlobalPrivilage(ApplicationUser user, GlobalPrivilage.Privilege privilage)
|
||||||
|
{
|
||||||
|
var have = false;
|
||||||
|
|
||||||
|
foreach (var priv in privilage.GetPrivilegesFor())
|
||||||
|
{
|
||||||
|
have = await UserHasPrivilage(user, priv);
|
||||||
|
if (have) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return have;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RolePrivilage?> GetPrivilageByNameIfDontExistCreateAsync(string privilageName)
|
||||||
|
{
|
||||||
|
var privilage = await GetPrivilageByName(privilageName);
|
||||||
|
|
||||||
|
if (privilage == null)
|
||||||
|
{
|
||||||
|
privilage = new RolePrivilage(privilage: privilageName);
|
||||||
|
context.RolePrivilages.Add(privilage);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return privilage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RolePrivilage?> GetPrivilageByName(string privilage)
|
||||||
|
{
|
||||||
|
return await context.RolePrivilages.FirstOrDefaultAsync(q => q.Privilage == privilage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CreateServiceRoleResult> CreatePrivageAsync(string privilage)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var privilageObj = new RolePrivilage() { Privilage = privilage };
|
||||||
|
|
||||||
|
var entityEntry = context.RolePrivilages.Add(privilageObj);
|
||||||
|
|
||||||
|
if (await context.SaveChangesAsync() > 0) return new CreateServiceRoleResult(isSuccessed: true, null) { };
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> errors = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
errors[e.Source ?? "Uknow"] = e.Message;
|
||||||
|
return new CreateServiceRoleResult(false, errors);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CreateServiceRoleResult(false, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace OpenWarehouse.auth.api.Common.RegisterTokenManager;
|
||||||
|
|
||||||
|
public interface IRegisterTokenManager
|
||||||
|
{
|
||||||
|
public Task<RegisterToken> AddNewToken(string token, ApplicationUser creator, List<string> avaliableRole);
|
||||||
|
public Task<RegisterToken> AddNewTolken(string token, ApplicationUser creator, List<ApplicationRole> avaliableRole);
|
||||||
|
public Task<RegisterToken?> GetToken(string token);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.RegisterTokenManager;
|
||||||
|
|
||||||
|
public class RegisterTokenManager(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
RoleManager<ApplicationRole> roleManager,
|
||||||
|
ApplicationDbContext context
|
||||||
|
) : IRegisterTokenManager
|
||||||
|
{
|
||||||
|
public async Task<RegisterToken> AddNewToken(string token, ApplicationUser creator, List<string> avaliableRole)
|
||||||
|
{
|
||||||
|
var listRole = new List<ApplicationRole>();
|
||||||
|
foreach (var role in avaliableRole)
|
||||||
|
{
|
||||||
|
var roleObj = await roleManager.FindByNameAsync(role);
|
||||||
|
|
||||||
|
if (roleObj != null) listRole.Add(roleObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await AddNewTolken(token, creator, listRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RegisterToken> AddNewTolken(string token, ApplicationUser creator, List<ApplicationRole> avaliableRole)
|
||||||
|
{
|
||||||
|
var tokenObj = new RegisterToken()
|
||||||
|
{
|
||||||
|
Creator = creator,
|
||||||
|
AvaliableRole = avaliableRole,
|
||||||
|
Token = token
|
||||||
|
};
|
||||||
|
context.RegisterTokens.Add(tokenObj);
|
||||||
|
|
||||||
|
if (await context.SaveChangesAsync() > 0)
|
||||||
|
{
|
||||||
|
return tokenObj;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new DataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RegisterToken?> GetToken(string token)
|
||||||
|
{
|
||||||
|
return await context.RegisterTokens.FirstOrDefaultAsync(registerToken => registerToken.Token.Equals(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using OpenWarehouse.auth.lib.Model.Role;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Service;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.ServiceManager;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface IServiceManager
|
||||||
|
{
|
||||||
|
public Task<bool> CheckIfServiceUserExist(string serviceName);
|
||||||
|
public Task<bool> CreateServiceUserResult(string serviceName, bool serviceIsAdmin = false);
|
||||||
|
public Task<bool> AddAdminToServiceUser(string serviceName);
|
||||||
|
public Task<List<CreateServiceRoleResult>> CreateServiceRoles(List<RoleModel> serviceRoles, bool forcePrivilages = false);
|
||||||
|
public Task<List<CreateServicePrivilageResult>> CreateServicesPrivilage(List<string> servicePrivilages);
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using Kafka.Lib.Auth;
|
||||||
|
using MassTransit;
|
||||||
|
using OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
using OpenWarehouse.auth.api.Common.Exemptions;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Role;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Service;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Common.ServiceManager;
|
||||||
|
|
||||||
|
public class ServiceManager(
|
||||||
|
UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager,
|
||||||
|
PrivilageManager.PrivilegeManager privilegeManager, ILogger<ServiceManager> logger,
|
||||||
|
ITopicProducer<UserChangeMessageType> userTopic) : IServiceManager
|
||||||
|
{
|
||||||
|
|
||||||
|
public async Task<bool> CheckIfServiceUserExist(string serviceName)
|
||||||
|
{
|
||||||
|
return await userManager.FindByNameAsync($"Service-{serviceName}") != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CreateServiceUserResult(string serviceName, bool serviceIsAdmin = false)
|
||||||
|
{
|
||||||
|
var serviceUser = new ApplicationUser()
|
||||||
|
{
|
||||||
|
UserName = $"Service-{serviceName}",
|
||||||
|
Email = $"service-{serviceName}@service.local", LockoutEnabled = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await userManager.CreateAsync(serviceUser);
|
||||||
|
if (!result.Succeeded) throw new Exception(result.ToString());
|
||||||
|
if (serviceIsAdmin) await AddAdminToServiceUser(serviceName);
|
||||||
|
|
||||||
|
var addClaim = await userManager.AddClaimAsync(serviceUser, AccountType.Service.Claim());
|
||||||
|
|
||||||
|
var topic = new UserChangeMessageType()
|
||||||
|
{
|
||||||
|
UserId = serviceUser.Id,
|
||||||
|
Changes = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Username", $"Service-{serviceName}"},
|
||||||
|
{"AccountType", AccountType.Service.GetValue()},
|
||||||
|
{"IsGlobalAdmin", serviceIsAdmin.ToString()}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = userTopic.Produce(topic);
|
||||||
|
|
||||||
|
return result.Succeeded && addClaim.Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddAdminToServiceUser(string serviceName)
|
||||||
|
{
|
||||||
|
var roleName = $"Service-{serviceName}";
|
||||||
|
var user = await userManager.FindByNameAsync(roleName) ?? throw new AccountExemptions(false, "Account not found");
|
||||||
|
var isAllready = await privilegeManager.UserHasGlobalPrivilage(user, GlobalPrivilage.Privilege.GlobalAdmin);
|
||||||
|
|
||||||
|
if (isAllready) return true;
|
||||||
|
|
||||||
|
isAllready = await roleManager.FindByNameAsync(roleName) != null;
|
||||||
|
|
||||||
|
if (!isAllready)
|
||||||
|
{
|
||||||
|
var serviceRole = new ApplicationRole()
|
||||||
|
{
|
||||||
|
IsGlobalAdmin = true,
|
||||||
|
Name = roleName
|
||||||
|
};
|
||||||
|
var serviceRoleResult = await roleManager.CreateAsync(serviceRole);
|
||||||
|
if (!serviceRoleResult.Succeeded)
|
||||||
|
{
|
||||||
|
throw new RoleExemptions(false, serviceRoleResult.ToString(), serviceRoleResult
|
||||||
|
.Errors.Select(error => new KeyValuePair<string, string>(error.Code, error.Description))
|
||||||
|
.ToDictionary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var addRes = await userManager.AddToRoleAsync(user, roleName);
|
||||||
|
|
||||||
|
var topic = new UserChangeMessageType()
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Changes = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Role", isAllready ? "Allready exist" : "Created"},
|
||||||
|
{"Operation", "Added admin to user"},
|
||||||
|
{"Sucess", addRes.Succeeded.ToString()}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = userTopic.Produce(topic);
|
||||||
|
|
||||||
|
return addRes.Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<CreateServiceRoleResult>> CreateServiceRoles(List<RoleModel> serviceRoles,
|
||||||
|
bool forcePrivilages = false)
|
||||||
|
{
|
||||||
|
logger.LogDebug($"Create roles: {serviceRoles.Select(q => q.RoleName).ToList()}");
|
||||||
|
var result = new List<CreateServiceRoleResult>();
|
||||||
|
foreach (var role in serviceRoles)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> errors = new();
|
||||||
|
var roleApp = new ApplicationRole()
|
||||||
|
{
|
||||||
|
IsGlobalAdmin = role.IsGlobalAdmin,
|
||||||
|
Name = role.RoleName
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var privilage in role.Privilage ?? throw new Exception("Role is null"))
|
||||||
|
{
|
||||||
|
RolePrivilage? privilageObj;
|
||||||
|
if (forcePrivilages)
|
||||||
|
{
|
||||||
|
privilageObj = await privilegeManager.GetPrivilageByName(privilage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
privilageObj = await privilegeManager.GetPrivilageByNameIfDontExistCreateAsync(privilage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privilageObj != null)
|
||||||
|
{
|
||||||
|
roleApp.Privilege.Add(privilageObj);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors[privilage] = "Not found";
|
||||||
|
logger.LogWarning($"Privilage {privilage} not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultRole = await roleManager.CreateAsync(roleApp);
|
||||||
|
if (!resultRole.Succeeded)
|
||||||
|
{
|
||||||
|
_ = resultRole.Errors.Select(errorL => errors[errorL.Code] = errorL.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new CreateServiceRoleResult(resultRole.Succeeded, errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CreateServicePrivilageResult>> CreateServicesPrivilage(List<string> servicePrivilages)
|
||||||
|
{
|
||||||
|
List<CreateServicePrivilageResult> results = new();
|
||||||
|
foreach (var privilage in servicePrivilages)
|
||||||
|
{
|
||||||
|
var resultsLocal = await privilegeManager.CreatePrivageAsync(privilage);
|
||||||
|
|
||||||
|
logger.LogDebug($"Create new role {privilage} is sucess {resultsLocal.IsSuccessed} with errors {resultsLocal.Errors}");
|
||||||
|
|
||||||
|
results.Add(new CreateServicePrivilageResult(privilage, resultsLocal.IsSuccessed));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Net.Mime;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Kafka.Lib.Auth;
|
||||||
|
using Kafka.Lib.Auth.Model;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
using OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
using OpenWarehouse.auth.lib.Model;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Controller;
|
||||||
|
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]/[action]")]
|
||||||
|
public class Account(
|
||||||
|
RoleManager<ApplicationRole> roleManager,
|
||||||
|
PrivilegeManager privilegeManager,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
AuthTokenRepo tokenRepo,
|
||||||
|
ITopicProducer<UserChangeMessageType> userChangeProducer,
|
||||||
|
ITopicProducer<UserChangeClaimsMessageType> userClaimChangeProducer,
|
||||||
|
ILogger<Account> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpDelete]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalDeleteUser)]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<DeleteUserReturnModel>> DeleteUser([FromBody]DeleteUserRequestModel requestModel)
|
||||||
|
{
|
||||||
|
logger.LogInformation("DeleteUser called with UserId: {UserId}", requestModel.UserId);
|
||||||
|
|
||||||
|
if (requestModel.UserId != null)
|
||||||
|
{
|
||||||
|
var user = await userManager.FindByIdAsync(requestModel.UserId);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("User found: {UserId}", user.Id);
|
||||||
|
|
||||||
|
var tokenR = await tokenRepo.DeleteAllUserTokenAsync(user);
|
||||||
|
var userR = await userManager.DeleteAsync(user);
|
||||||
|
DeleteUserReturnModel returnModel = new DeleteUserReturnModel
|
||||||
|
{
|
||||||
|
UserId = requestModel.UserId,
|
||||||
|
DedeltedTokenCount = tokenR,
|
||||||
|
IsOperationSuccess = userR.Succeeded,
|
||||||
|
OperationMessage = string.Join(";", userR.Errors)
|
||||||
|
};
|
||||||
|
|
||||||
|
var topic = new UserChangeMessageType()
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Changes = new Dictionary<string, string> { { "AccountChange", "Delete" } },
|
||||||
|
Id = HttpContext.TraceIdentifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = userChangeProducer.Produce(topic);
|
||||||
|
|
||||||
|
logger.LogInformation("User deleted: {UserId}", user.Id);
|
||||||
|
|
||||||
|
return Ok(returnModel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("User not found: {UserId}", requestModel.UserId);
|
||||||
|
return BadRequest("User not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("DeleteUser request with empty UserId");
|
||||||
|
return BadRequest("User cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalCreateUser)]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<CreateUserModelReturn>> CreateUser([FromBody] CreateUserModel newUser)
|
||||||
|
{
|
||||||
|
logger.LogInformation("CreateUser called with Username: {Username}, Email: {Email}", newUser.Username, newUser.Email);
|
||||||
|
|
||||||
|
var userReturn = new CreateUserModelReturn()
|
||||||
|
{
|
||||||
|
Username = newUser.Username,
|
||||||
|
Email = newUser.Email,
|
||||||
|
Password = "*******",
|
||||||
|
Roles = new List<CreateUserModelReturn.RoleStatus>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newUser is { Username: not null, Email: not null, Password: not null })
|
||||||
|
{
|
||||||
|
var user = new ApplicationUser {UserName = newUser.Username, Email = newUser.Email,
|
||||||
|
EmailConfirmed = true, DialCode = newUser.Dialcode, PhoneNumber = newUser.Phone};
|
||||||
|
|
||||||
|
var userResult = await userManager.CreateAsync(user, newUser.Password);
|
||||||
|
if (userResult.Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogInformation("User created: {UserId}", user.Id);
|
||||||
|
|
||||||
|
var resultClaim = await userManager.AddClaimAsync(user, AccountType.User.Claim());
|
||||||
|
|
||||||
|
if (resultClaim.Succeeded && newUser.Roles != null)
|
||||||
|
{
|
||||||
|
userReturn.IsCreated = true;
|
||||||
|
userReturn.NewUserId = user.Id;
|
||||||
|
|
||||||
|
foreach (var role in newUser.Roles)
|
||||||
|
{
|
||||||
|
var appRole = await roleManager.FindByNameAsync(role);
|
||||||
|
|
||||||
|
CreateUserModelReturn.RoleStatus roleStatus = new CreateUserModelReturn.RoleStatus();
|
||||||
|
var result = await userManager.AddToRoleAsync(user, role);
|
||||||
|
|
||||||
|
roleStatus.Added = result.Succeeded;
|
||||||
|
roleStatus.Name = role;
|
||||||
|
roleStatus.Exist = appRole != null;
|
||||||
|
roleStatus.Mess = JsonSerializer.Serialize(string.Join(";", result.Errors));
|
||||||
|
|
||||||
|
userReturn.Roles.Add(roleStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
var topicMess = new UserChangeMessageType
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Id = user.Id,
|
||||||
|
Changes =
|
||||||
|
{
|
||||||
|
["RoleChange"] = string
|
||||||
|
.Join(";", userReturn.Roles
|
||||||
|
.Select(status => $"{status.Name}:{status.Added}/{status.Exist}")),
|
||||||
|
["UserChange"] = "Created",
|
||||||
|
["Success"] = $"{userReturn.IsCreated}"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await userChangeProducer.Produce(topicMess);
|
||||||
|
|
||||||
|
logger.LogInformation("User created successfully: {UserId}", user.Id);
|
||||||
|
return Ok(userReturn);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userReturn.ReturnMess = string.Join(';', userResult.Errors.Select(q => $"{q.Code}:{q.Description}"));
|
||||||
|
logger.LogWarning("Failed to add claims or roles to user: {UserId}", user.Id);
|
||||||
|
return StatusCode(406, userReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userReturn.ReturnMess = JsonSerializer.Serialize(string.Join(";", userResult.Errors.Select(q => $"{q.Code}:{q.Description}")));
|
||||||
|
logger.LogWarning("Failed to create user: {Username}", newUser.Username);
|
||||||
|
userReturn.IsCreated = false;
|
||||||
|
return StatusCode(406, userReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("Invalid CreateUser request");
|
||||||
|
userReturn.IsCreated = false;
|
||||||
|
userReturn.ReturnMess = "Invalid request";
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(406, userReturn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<ChangePasswordReturn>> ChangePassword([FromBody] ChangePassword req)
|
||||||
|
{
|
||||||
|
logger.LogInformation("ChangePassword called for UserId: {UserId}", User.FindFirstValue(ClaimTypes.NameIdentifier));
|
||||||
|
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var user = await userManager.FindByIdAsync(userId ?? "");
|
||||||
|
|
||||||
|
if ((user != null) && (req is { Password: not null, NewPassword1: not null, NewPassword2: not null }))
|
||||||
|
{
|
||||||
|
if (req.NewPassword1.Equals(req.NewPassword2))
|
||||||
|
{
|
||||||
|
var result = await userManager.ChangePasswordAsync(user, req.Password, req.NewPassword1);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Password changed successfully for UserId: {UserId}", user.Id);
|
||||||
|
var count = 0;
|
||||||
|
if (req.LogOutAllSession) count = await tokenRepo.LockAllUserTokenAsync(user);
|
||||||
|
return Ok(new ChangePasswordReturn { Mess = "Operation success.", IsSuccess = true, LockedTokenCount = count});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to change password for UserId: {UserId}", user.Id);
|
||||||
|
return StatusCode(418, new ChangePasswordReturn {Mess = string.Join(";", result.Errors
|
||||||
|
.Select(q => q.Description) ), IsSuccess = false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("New passwords do not match for UserId: {UserId}", user.Id);
|
||||||
|
return StatusCode(418, new ChangePasswordReturn {Mess = "New password not match.", IsSuccess = false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("Invalid ChangePassword request for UserId: {UserId}", userId);
|
||||||
|
return StatusCode(400, new ChangePasswordReturn {Mess = "Bad request", IsSuccess = false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalDeactivateUserToken)]
|
||||||
|
public async Task<ActionResult<OkModel>> DeactivateToken([FromBody] DeactiveTokenModel req)
|
||||||
|
{
|
||||||
|
logger.LogInformation("DeactivateToken called for Token: {Token}", req.Token);
|
||||||
|
var i = await tokenRepo.LockTokenAsync(req.Token ?? throw new Exception("Token is null"));
|
||||||
|
var resultMessage = i > 0 ? "Token lock success." : "Token Lock Fail.";
|
||||||
|
logger.LogInformation(resultMessage);
|
||||||
|
return new OkModel(resultMessage, i > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalEditUser)]
|
||||||
|
public async Task<ActionResult<AddClaimsToUserReturnModel>> AddClaimsToUser([FromBody] AddClaimsToUserModel req)
|
||||||
|
{
|
||||||
|
logger.LogInformation("AddClaimsToUser called for UserId: {UserId}", req.UserId);
|
||||||
|
var userId = req.UserId;
|
||||||
|
var user = await userManager.FindByIdAsync(userId) ?? throw new Exception("User not found");
|
||||||
|
var topic = new UserChangeClaimsMessageType()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Id = HttpContext.TraceIdentifier,
|
||||||
|
ClaimChages = new List<ClaimChagesModel>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionResult = new AddClaimsToUserReturnModel() { UserId = userId };
|
||||||
|
|
||||||
|
var curClaims = await userManager.GetClaimsAsync(user);
|
||||||
|
|
||||||
|
foreach (var newClaim in req.NewClaims)
|
||||||
|
{
|
||||||
|
var existingClaim = curClaims.FirstOrDefault(c => c.Type == newClaim.Key);
|
||||||
|
|
||||||
|
topic.ClaimChages.Add(new ClaimChagesModel()
|
||||||
|
{
|
||||||
|
Old = existingClaim != null ?
|
||||||
|
new ClaimModel
|
||||||
|
{
|
||||||
|
ClaimType = existingClaim.Type,
|
||||||
|
ClaimValue = existingClaim.Value,
|
||||||
|
ClaimIssuer = existingClaim.Issuer
|
||||||
|
} : null,
|
||||||
|
New = new ClaimModel
|
||||||
|
{
|
||||||
|
ClaimType = newClaim.Key,
|
||||||
|
ClaimValue = newClaim.Value,
|
||||||
|
ClaimIssuer = req.Issuer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultAddClaims = await userManager.AddClaimsAsync(user,
|
||||||
|
req.NewClaims.Select(pair => new Claim(type: pair.Key, value: pair.Value, null, req.Issuer)).ToList());
|
||||||
|
|
||||||
|
if (!resultAddClaims.Succeeded)
|
||||||
|
{
|
||||||
|
actionResult.IsSucess = false;
|
||||||
|
actionResult.Error = string.Join(";", resultAddClaims.Errors.Select(error => error.Description));
|
||||||
|
|
||||||
|
topic.IsSuccess = false;
|
||||||
|
topic.Errors =
|
||||||
|
resultAddClaims.Errors.Select(error =>
|
||||||
|
new KeyValuePair<string, string>(error.Code, error.Description))
|
||||||
|
.ToDictionary();
|
||||||
|
|
||||||
|
logger.LogWarning("Failed to add claims to user: {UserId}", userId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actionResult.IsSucess = true;
|
||||||
|
topic.IsSuccess = true;
|
||||||
|
logger.LogInformation("Claims added successfully to UserId: {UserId}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = userClaimChangeProducer.Produce(topic);
|
||||||
|
|
||||||
|
var allUserClaim = await userManager.GetClaimsAsync(user);
|
||||||
|
|
||||||
|
foreach (var claim in allUserClaim)
|
||||||
|
{
|
||||||
|
actionResult.AllUserClaims[claim.Type] = claim.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(actionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<AddClaimsToUserReturnModel>> GetAllClaimsUser()
|
||||||
|
{
|
||||||
|
var userId = User.Claims.Where(q => q.Type == ClaimTypes.NameIdentifier)
|
||||||
|
.Select(q => q.Value).FirstOrDefault();
|
||||||
|
logger.LogInformation("GetAllClaimsUser called for UserId: {UserId}", userId);
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId ?? "") ?? throw new Exception("User not found");
|
||||||
|
|
||||||
|
var result = new AddClaimsToUserReturnModel() { UserId = userId, IsSucess = true, Error = null};
|
||||||
|
|
||||||
|
var allUserClaim = await userManager.GetClaimsAsync(user);
|
||||||
|
foreach (var claim in allUserClaim)
|
||||||
|
{
|
||||||
|
result.AllUserClaims[claim.Type] = claim.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<ChangeEmailRespond>> UpdateUserEmail([FromBody] ChangeEmailRequest request)
|
||||||
|
{
|
||||||
|
logger.LogInformation("UpdateUserEmail called with NewEmail: {NewEmail}", request.NewEmail);
|
||||||
|
|
||||||
|
var response = new ChangeEmailRespond
|
||||||
|
{
|
||||||
|
NewEmail = request.NewEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
response.Problems.Add("UserId", "User ID not found in token.");
|
||||||
|
logger.LogWarning("UserId not found in token");
|
||||||
|
return BadRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
response.Problems.Add("User", "User not found.");
|
||||||
|
logger.LogWarning("User not found: {UserId}", userId);
|
||||||
|
return Unauthorized(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OldEmail = user.Email;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(request.NewEmail))
|
||||||
|
{
|
||||||
|
response.Problems.Add("NewEmail", "New email is required.");
|
||||||
|
logger.LogWarning("New email is required for UserId: {UserId}", userId);
|
||||||
|
return BadRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Email = request.NewEmail;
|
||||||
|
var result = await userManager.UpdateAsync(user);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
response.ValSuccess = true;
|
||||||
|
response.UpdateSuccess = true;
|
||||||
|
logger.LogInformation("User email updated successfully: {UserId}", userId);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.ValSuccess = true;
|
||||||
|
response.Problems = result.Errors.ToDictionary(
|
||||||
|
error => error.Code,
|
||||||
|
error => error.Description
|
||||||
|
);
|
||||||
|
logger.LogWarning("Failed to update user email for UserId: {UserId}", userId);
|
||||||
|
return StatusCode(418, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<ChangePhoneNumberRespond>> UpdatePhoneNumber([FromBody] ChangePhoneNumberRequest request)
|
||||||
|
{
|
||||||
|
logger.LogInformation("UpdatePhoneNumber called with Prefix: {Prefix}, Number: {Number}", request.Prefix, request.Number);
|
||||||
|
|
||||||
|
var response = new ChangePhoneNumberRespond
|
||||||
|
{
|
||||||
|
Prefix = request.Prefix,
|
||||||
|
Number = request.Number
|
||||||
|
};
|
||||||
|
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
response.Problems.Add("UserId", "User ID not found in token.");
|
||||||
|
logger.LogWarning("UserId not found in token");
|
||||||
|
return BadRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
response.Problems.Add("User", "User not found.");
|
||||||
|
logger.LogWarning("User not found: {UserId}", userId);
|
||||||
|
return Unauthorized(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OldPrefix = user.DialCode;
|
||||||
|
response.OldPhone = user.PhoneNumber;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(request.Prefix) || string.IsNullOrEmpty(request.Number))
|
||||||
|
{
|
||||||
|
response.Problems.Add("PhoneNumber", "Prefix and number are required.");
|
||||||
|
logger.LogWarning("Prefix or number missing for UserId: {UserId}", userId);
|
||||||
|
return BadRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PhoneNumber = request.Number;
|
||||||
|
user.DialCode = request.Prefix;
|
||||||
|
|
||||||
|
var result = await userManager.UpdateAsync(user);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
response.ValSuccess = true;
|
||||||
|
response.UpdateSuccess = true;
|
||||||
|
logger.LogInformation("User phone number updated successfully: {UserId}", userId);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.ValSuccess = true;
|
||||||
|
response.Problems = result.Errors.ToDictionary(
|
||||||
|
error => error.Code,
|
||||||
|
error => error.Description
|
||||||
|
);
|
||||||
|
logger.LogWarning("Failed to update phone number for UserId: {UserId}", userId);
|
||||||
|
return StatusCode(418, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<AccountDetailRespond>> GetMyDetail()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var response = new AccountDetailRespond();
|
||||||
|
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
response.Problems.Add("UserId", "User ID not found in token.");
|
||||||
|
logger.LogWarning("UserId not found in token");
|
||||||
|
return BadRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
response.Problems.Add("User", "User not found.");
|
||||||
|
logger.LogWarning("User not found: {UserId}", userId);
|
||||||
|
return Unauthorized(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Id = user.Id;
|
||||||
|
response.Email = user.Email;
|
||||||
|
response.UserName = user.UserName;
|
||||||
|
response.Phone = user.PhoneNumber;
|
||||||
|
response.DialCode = user.DialCode;
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalEditUser, GlobalPrivilage.Privilege.GlobalDeleteUser)]
|
||||||
|
public async Task<ActionResult<AccountListsResponse>> GetListOfUsers([FromBody] AccountListsRequest request)
|
||||||
|
{
|
||||||
|
if (request.Page < 1 || request.PageSize < 1)
|
||||||
|
{
|
||||||
|
return BadRequest("Page and page size must be greater than 0.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new AccountListsResponse
|
||||||
|
{
|
||||||
|
AccountsDetail = await userManager.Users
|
||||||
|
.OrderBy(u => u.UserName)
|
||||||
|
.Skip((request.Page - 1) * request.PageSize)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.Select(q
|
||||||
|
=>new AccountDetailRespond()
|
||||||
|
{
|
||||||
|
UserName = q.UserName, DialCode = q.DialCode, Email = q.Email, Id = q.Id, Phone = q.PhoneNumber
|
||||||
|
}).ToListAsync(),
|
||||||
|
Page = request.Page,
|
||||||
|
PageSize = request.PageSize,
|
||||||
|
NextPageExist = await userManager.Users
|
||||||
|
.OrderBy(u => u.UserName)
|
||||||
|
.Skip((request.Page) * request.PageSize)
|
||||||
|
.Take(request.PageSize).AnyAsync(),
|
||||||
|
LastPageNum = (await userManager.Users.CountAsync()) / request.PageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalEditUser)]
|
||||||
|
public async Task<ActionResult<EditAccountRespond>> EditAccount([FromBody] EditAccountRequest request)
|
||||||
|
{
|
||||||
|
if (request is { UserId: null })
|
||||||
|
{
|
||||||
|
return BadRequest(new EditAccountRespond()
|
||||||
|
{
|
||||||
|
Message = "Invalid request", Success = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(request.UserId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return BadRequest(new EditAccountRespond()
|
||||||
|
{
|
||||||
|
Message = "User not found", Success = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var respond = new EditAccountRespond();
|
||||||
|
|
||||||
|
if (user.UserName != request.NewUserName)
|
||||||
|
{
|
||||||
|
respond.NewUserName = request.NewUserName;
|
||||||
|
user.UserName = request.NewUserName;
|
||||||
|
respond.UserNameEdited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Email != request.NewEmail)
|
||||||
|
{
|
||||||
|
respond.NewEmail = request.NewEmail;
|
||||||
|
user.Email = request.NewEmail;
|
||||||
|
respond.EmailEdited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.DialCode != request.NewDialCode || user.PhoneNumber != request.NewPhone)
|
||||||
|
{
|
||||||
|
respond.NewDialCode = request.NewDialCode;
|
||||||
|
respond.NewPhone = request.NewPhone;
|
||||||
|
|
||||||
|
user.PhoneNumber = request.NewPhone;
|
||||||
|
user.DialCode = request.NewDialCode;
|
||||||
|
respond.PhoneEdited = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await userManager.UpdateAsync(user);
|
||||||
|
|
||||||
|
respond.Success = result.Succeeded;
|
||||||
|
respond.Message = string.Join("\n",result.Errors.Select(error => $"{error.Code}:{error.Description}"));
|
||||||
|
return respond;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
using System.Net.Mime;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
using OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
using OpenWarehouse.auth.api.Common.RegisterTokenManager;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Controller;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("[action]")]
|
||||||
|
public class Auth(
|
||||||
|
SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager,
|
||||||
|
AuthTokenRepo tokenRepo, ApplicationDbContext applicationDbContext,
|
||||||
|
RegisterTokenManager registerTokenManager,
|
||||||
|
ILogger<Auth> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, MediaTypeNames.Application.Xml)]
|
||||||
|
public async Task<ActionResult<LoginModel>> Login([FromBody] LoginRequestModel requestModel)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Login request received for user: {Login}", requestModel.Login);
|
||||||
|
if (requestModel.Pass == null || requestModel.Login == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Invalid login request model.");
|
||||||
|
return BadRequest("Invalid request model.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginModel = new LoginModel();
|
||||||
|
|
||||||
|
if (requestModel.Pass == null)
|
||||||
|
{
|
||||||
|
loginModel.AccountProblem = "Login and password are required.";
|
||||||
|
logger.LogWarning("Password is missing in the login request for user: {Login}", requestModel.Login);
|
||||||
|
return BadRequest(loginModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var applicationUser = await userManager.FindByNameAsync(requestModel.Login)
|
||||||
|
?? await userManager.FindByEmailAsync(requestModel.Login);
|
||||||
|
if (applicationUser == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("User not found for login: {Login}", requestModel.Login);
|
||||||
|
loginModel.Message = "Incorrect login or password.";
|
||||||
|
return Unauthorized(loginModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
loginModel.Login = applicationUser.UserName;
|
||||||
|
loginModel.Email = applicationUser.Email;
|
||||||
|
|
||||||
|
var claims = await userManager.GetClaimsAsync(applicationUser);
|
||||||
|
|
||||||
|
if (claims.Any(claim => claim.Type.Equals(AccountType.Service.GetValue())))
|
||||||
|
{
|
||||||
|
logger.LogWarning("Service account attempted to login: {Login}", requestModel.Login);
|
||||||
|
return Forbid("This is a service account.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationUser.LockoutEnabled)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Locked account attempted to login: {Login}", requestModel.Login);
|
||||||
|
return Forbid("Account is locked.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var signInResult = await signInManager.CheckPasswordSignInAsync(applicationUser, requestModel.Pass, false);
|
||||||
|
|
||||||
|
if (signInResult.Succeeded)
|
||||||
|
{
|
||||||
|
// var expiration = DateTime.UtcNow.AddHours(1);
|
||||||
|
|
||||||
|
var expiration = DateTime.UtcNow.AddMinutes(30);
|
||||||
|
loginModel.Token = await tokenRepo.CreateJwtToken(applicationUser, expiration, null);
|
||||||
|
loginModel.Exp = expiration.ToBinary();
|
||||||
|
loginModel.IsLogged = true;
|
||||||
|
logger.LogInformation("User logged in successfully: {Login}", requestModel.Login);
|
||||||
|
return Ok(loginModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signInResult.RequiresTwoFactor)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Two-factor authentication required for user: {Login}", requestModel.Login);
|
||||||
|
loginModel.Message = "Two-factor authentication is required.";
|
||||||
|
loginModel.RequiresTwoFactor = true;
|
||||||
|
return Ok(loginModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("Incorrect login or password for user: {Login}", requestModel.Login);
|
||||||
|
loginModel.Message = "Incorrect login or password.";
|
||||||
|
return Unauthorized(loginModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<RegisterReturnModel>> Register([FromBody]RegisterModel registerModel)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Registration request received for user: {Username}", registerModel.Username);
|
||||||
|
var registerReturnModel = new RegisterReturnModel();
|
||||||
|
registerReturnModel.IsCreated = true;
|
||||||
|
if (registerModel.Username != null)
|
||||||
|
{
|
||||||
|
var user = new ApplicationUser(registerModel.Username);
|
||||||
|
user.Email = registerModel.Email;
|
||||||
|
|
||||||
|
if (registerModel.RegisterToken != null)
|
||||||
|
{
|
||||||
|
var regToken = await registerTokenManager.GetToken(registerModel.RegisterToken);
|
||||||
|
|
||||||
|
if (regToken == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Registration token not found: {RegisterToken}", registerModel.RegisterToken);
|
||||||
|
registerReturnModel.IsCreated = false;
|
||||||
|
registerReturnModel.ProblemWithToken = "Token Not found;";
|
||||||
|
return Unauthorized(registerReturnModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regToken.IsUsed)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Registration token already used: {RegisterToken}", registerModel.RegisterToken);
|
||||||
|
registerReturnModel.IsCreated = false;
|
||||||
|
registerReturnModel.ProblemWithToken = "Token is alleredy used;";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( registerModel.Password ?? "" ).Length > 6)
|
||||||
|
{
|
||||||
|
registerReturnModel.IsCreated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registerReturnModel.IsCreated) return Unauthorized(registerReturnModel);
|
||||||
|
|
||||||
|
if (registerModel.Password != null)
|
||||||
|
{
|
||||||
|
var result = await userManager.CreateAsync(user, registerModel.Password);
|
||||||
|
var resultClaim = await userManager.AddClaimAsync(user, AccountType.User.Claim());
|
||||||
|
if (result.Succeeded && resultClaim.Succeeded)
|
||||||
|
{
|
||||||
|
regToken.IsUsed = true;
|
||||||
|
await applicationDbContext.SaveChangesAsync();
|
||||||
|
registerReturnModel.AuthToken =
|
||||||
|
await tokenRepo.CreateJwtToken(user, DateTime.UtcNow.AddHours(1), null);
|
||||||
|
logger.LogInformation("User registered successfully: {Username}", registerModel.Username);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var error in result.Errors)
|
||||||
|
{
|
||||||
|
logger.LogError("Error during user registration: {Code} - {Description}", error.Code, error.Description);
|
||||||
|
registerReturnModel.OtherWithOtherProblem += error.Code + "-" + error.Description + ";";
|
||||||
|
return Unauthorized(registerReturnModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("Registration token is required but missing for user: {Username}", registerModel.Username);
|
||||||
|
return BadRequest("Token are required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Ok(registerReturnModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
public async Task<LogOutModel> LogOut()
|
||||||
|
{
|
||||||
|
logger.LogInformation("Logout request received.");
|
||||||
|
if (Request.Headers.TryGetValue("Authorization", out var token))
|
||||||
|
{
|
||||||
|
var tokenStr = token.ToString();
|
||||||
|
string?[] tokenSplit = tokenStr.Split(" ");
|
||||||
|
if (tokenSplit.Length > 0) tokenStr = tokenSplit[1];
|
||||||
|
if (await tokenRepo.LockTokenAsync(tokenStr) > 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation("User logged out successfully.");
|
||||||
|
return new LogOutModel(true, "Logout successful");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.LogWarning("Logout failed due to missing or invalid token.");
|
||||||
|
return new LogOutModel(false, "Logout Fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
public async Task<ActionResult<RegeneratedTokenModel>> RegenerateToken()
|
||||||
|
{
|
||||||
|
logger.LogInformation("Token regeneration request received.");
|
||||||
|
Request.Headers.TryGetValue("Authorization", out var token);
|
||||||
|
|
||||||
|
token = token.ToString().Split(" ")[1];
|
||||||
|
|
||||||
|
var newTokenStr = await tokenRepo.RegenerateJwtToken(token, DateTime.UtcNow.AddMinutes(30));
|
||||||
|
|
||||||
|
logger.LogInformation("Token regenerated successfully");
|
||||||
|
|
||||||
|
var respond = new RegeneratedTokenModel()
|
||||||
|
{
|
||||||
|
Mess = newTokenStr != null ? "Regeneration success" : "Regeneration fail",
|
||||||
|
Token = newTokenStr
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(respond);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalDeleteExpiredToken)]
|
||||||
|
public async Task<ActionResult<int>> DeleteExpiredToken()
|
||||||
|
{
|
||||||
|
logger.LogInformation("Delete expired tokens request received.");
|
||||||
|
return Ok(await tokenRepo.DeleteInactiveToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<bool>> TokenIsLock([FromBody] TokenAskIfIsLockModel req)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Check if token is locked request received.");
|
||||||
|
return Ok(await tokenRepo.IsTokenLockAsync(req.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Privilage;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Controller;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
[Route("[controller]/[action]")]
|
||||||
|
public class Privilege(UserManager<ApplicationUser> userManager,
|
||||||
|
PrivilegeManager privilegeManager, ILogger<Privilege> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<List<string>>> GetUserPrivilege()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
logger.LogInformation("GetUserPrivilege request received for user: {UserId}", userId);
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("User ID not found in claims");
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("User not found for ID: {UserId}", userId);
|
||||||
|
return Unauthorized("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var privilege = await privilegeManager.GetUserPrivilage(user);
|
||||||
|
logger.LogInformation("Privileges retrieved for user {UserId}", userId);
|
||||||
|
|
||||||
|
return Ok(privilege);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalEditRole)]
|
||||||
|
public async Task<ActionResult> CreatePrivilage([FromBody] CreatePrivilageModel privilage)
|
||||||
|
{
|
||||||
|
logger.LogInformation("CreatePrivilege request received for privilege: {Privilege}", privilage.Privilage);
|
||||||
|
if (privilage is { Privilage: null })
|
||||||
|
{
|
||||||
|
logger.LogWarning("Invalid privilege model provided");
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok = await privilegeManager.CreatePrivageAsync(privilage.Privilage);
|
||||||
|
|
||||||
|
if (ok.IsSuccessed)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Privilege created successfully");
|
||||||
|
return Created();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to create privilege");
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
using System.Net.Mime;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using OpenWarehouse.auth.lib.Model;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Role;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Controller;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
[Route("[controller]/[action]")]
|
||||||
|
public class Role(
|
||||||
|
UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager,
|
||||||
|
PrivilegeManager privilegeManager) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
|
||||||
|
public async Task<ActionResult<RoleListModel>> GetRoles()
|
||||||
|
{
|
||||||
|
var roles = await roleManager.Roles.ToListAsync();
|
||||||
|
var roleList = new RoleListModel();
|
||||||
|
foreach (var role in roles)
|
||||||
|
{
|
||||||
|
var roleC = new RoleModel() { RoleName = role.Name ?? "", IsGlobalAdmin = role.IsGlobalAdmin };
|
||||||
|
|
||||||
|
var privilage = await privilegeManager.GetRolePrivilage(role);
|
||||||
|
if (privilage != null)
|
||||||
|
roleC.Privilage = privilage;
|
||||||
|
roleList.Roles?.Add(roleC);
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleList;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<UserRoleModel>> GetUserRoles([FromBody] UserRoleReqestModel req)
|
||||||
|
{
|
||||||
|
if (req is { UserIdentify: not null })
|
||||||
|
{
|
||||||
|
var user = (await userManager.FindByNameAsync(req.UserIdentify) )
|
||||||
|
?? (await userManager.FindByEmailAsync(req.UserIdentify))
|
||||||
|
?? (await userManager.FindByIdAsync(req.UserIdentify));
|
||||||
|
if (user == null) return StatusCode(500, new ErrorModel(){StatusCode = 500, Message = "User not found"});
|
||||||
|
var rolesNames = await userManager.GetRolesAsync(user);
|
||||||
|
var userRoleModel = new UserRoleModel() { UserId = user.Id, Username = user.UserName, Roles = new List<RoleModel>()};
|
||||||
|
foreach (var roleName in rolesNames)
|
||||||
|
{
|
||||||
|
var role = await roleManager.FindByNameAsync(roleName);
|
||||||
|
if (role != null)
|
||||||
|
{
|
||||||
|
var roleC = new RoleModel
|
||||||
|
{
|
||||||
|
RoleName = role.Name ?? "", IsGlobalAdmin = role.IsGlobalAdmin,
|
||||||
|
Privilage = await privilegeManager.GetRolePrivilage(role)
|
||||||
|
};
|
||||||
|
userRoleModel.Roles.Add(roleC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(userRoleModel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return StatusCode(500, new ErrorModel(){StatusCode = 500, Message = "Invalid reqest"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalCreateRole)]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<OkModel>> CreateRole([FromBody] CreateRoleModel req)
|
||||||
|
{
|
||||||
|
if (req is { Name: not null })
|
||||||
|
{
|
||||||
|
var okModel = new OkModel();
|
||||||
|
var role = new ApplicationRole() { Name = req.Name, IsGlobalAdmin = false};
|
||||||
|
if (req is { Privilage: not null })
|
||||||
|
{
|
||||||
|
foreach (var privilageName in req.Privilage)
|
||||||
|
{
|
||||||
|
var privilage = req.ForcePrivilage
|
||||||
|
? await privilegeManager.GetPrivilageByName(privilageName)
|
||||||
|
: await privilegeManager.GetPrivilageByNameIfDontExistCreateAsync(privilageName);
|
||||||
|
if (privilage != null)
|
||||||
|
{
|
||||||
|
role.Privilege.Add(privilage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
okModel.Message += $"Privilage {privilageName} not found;";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await roleManager.CreateAsync(role);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
okModel.IsSuccess = true;
|
||||||
|
return Ok(okModel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
okModel.Message += string.Join(";", result.Errors.Select(error => error.Description));
|
||||||
|
okModel.IsSuccess = false;
|
||||||
|
return StatusCode(500, okModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return StatusCode(500, new ErrorModel(){StatusCode = 500, Message = "Invalid reqest"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalEditUser)]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<OkModel>> AddRoleToUser([FromBody] AddRoleToUserModel req)
|
||||||
|
{
|
||||||
|
if (req is { UserIdentify: not null, RoleIdentify: not null })
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
var user = (await userManager.FindByNameAsync(req.UserIdentify))
|
||||||
|
?? (await userManager.FindByEmailAsync(req.UserIdentify))
|
||||||
|
?? (await userManager.FindByIdAsync(req.UserIdentify));
|
||||||
|
var role = (await roleManager.FindByIdAsync(req.RoleIdentify))
|
||||||
|
?? (await roleManager.FindByNameAsync(req.RoleIdentify));
|
||||||
|
|
||||||
|
if (user is null || role is null)
|
||||||
|
{
|
||||||
|
return Problem("User or Role not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await userManager.AddToRoleAsync(user, role.Name);
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
var rolePriv = await privilegeManager.GetRolePrivilage(role);
|
||||||
|
var roleModel = new RoleModel()
|
||||||
|
{ IsGlobalAdmin = role.IsGlobalAdmin, RoleName = role.Name, Privilage = rolePriv };
|
||||||
|
return Ok(new OkModel() { Message = "Ok", IsSuccess = true, Value = roleModel });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Ok(new OkModel()
|
||||||
|
{ Message = string.Join(";", result.Errors.Select(error => error.Description)), IsSuccess = true });
|
||||||
|
}
|
||||||
|
}else
|
||||||
|
{
|
||||||
|
return Problem("Invalid request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalEditRole)]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<RoleModel>> EditRolePrivilege([FromBody]EditRoleModel req)
|
||||||
|
{
|
||||||
|
var role = (await roleManager.FindByIdAsync(req.RoleIdentify))
|
||||||
|
?? (await roleManager.FindByNameAsync(req.RoleIdentify));
|
||||||
|
if (role == null) Ok("Not found!");
|
||||||
|
foreach (var addPriv in req.AddPrivilage)
|
||||||
|
{
|
||||||
|
RolePrivilage? priv;
|
||||||
|
if (req.ForceAddedPrivilage)
|
||||||
|
{
|
||||||
|
priv = await privilegeManager.GetPrivilageByNameIfDontExistCreateAsync(addPriv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
priv = await privilegeManager.GetPrivilageByName(addPriv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priv is {Privilage: not null} && role != null) await privilegeManager.AddRolePrivilage(role, priv.Privilage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.DeletePrivilage != null)
|
||||||
|
foreach (var delPriv in req.DeletePrivilage)
|
||||||
|
{
|
||||||
|
await privilegeManager.DeleteRolePrivilage(role, delPriv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.NewName != null && req.NewName != "string")
|
||||||
|
{
|
||||||
|
if (role != null) role.Name = req.NewName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (role != null)
|
||||||
|
{
|
||||||
|
var privilage = await privilegeManager.GetRolePrivilage(role);
|
||||||
|
|
||||||
|
var roleModel = new RoleModel()
|
||||||
|
{ RoleName = role.Name, IsGlobalAdmin = role.IsGlobalAdmin, Privilage = privilage };
|
||||||
|
return Ok(roleModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Problem();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalDeleteRole)]
|
||||||
|
[Consumes(MediaTypeNames.Application.Json, otherContentTypes: [MediaTypeNames.Application.Xml])]
|
||||||
|
public async Task<ActionResult<OkModel>> DeleteRole([FromBody] DeleteRoleModel req)
|
||||||
|
{
|
||||||
|
if (req.RoleIdentify != null)
|
||||||
|
{
|
||||||
|
var role = (await roleManager.FindByIdAsync(req.RoleIdentify))
|
||||||
|
?? (await roleManager.FindByNameAsync(req.RoleIdentify));
|
||||||
|
if (role != null)
|
||||||
|
{
|
||||||
|
var result = await roleManager.DeleteAsync(role);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return Ok(new OkModel() { Message = "", IsSuccess = result.Succeeded });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Ok(new OkModel() { Message = string.Join(";", result.Errors.Select(error => error.Description)), IsSuccess = false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new OkModel() { Message = "Role not found.", IsSuccess = false });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Kafka.Lib.Auth;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
using OpenWarehouse.auth.api.Common.CustomIdentifyManager;
|
||||||
|
using OpenWarehouse.auth.api.Common.ServiceManager;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Service;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Controller;
|
||||||
|
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]/[action]")]
|
||||||
|
public class Service(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
IConfiguration configuration, AuthTokenRepo tokenRepo,
|
||||||
|
IServiceManager serviceManager, ILogger<Service> logger,
|
||||||
|
ITopicProducer<RegisterNewServiceMessageType> serviceTopic) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalAdmin)]
|
||||||
|
public async Task<ActionResult<CreateServiceTokenReturnModel>> GetTokenForService([FromBody] CreateServiceTokenModel req)
|
||||||
|
{
|
||||||
|
if (req is { ServiceName: not null, ServiceRoles: not null, ServicePrivilages: not null })
|
||||||
|
{
|
||||||
|
var result = new CreateServiceTokenReturnModel(req.ServiceName)
|
||||||
|
{
|
||||||
|
ServiceUsername = $"Service-{req.ServiceName}"
|
||||||
|
};
|
||||||
|
|
||||||
|
var userExist = await serviceManager.CheckIfServiceUserExist(req.ServiceName);
|
||||||
|
if (!userExist)
|
||||||
|
{
|
||||||
|
var userRes = await serviceManager.CreateServiceUserResult(req.ServiceName, true);
|
||||||
|
result.UserAllereadyExist = false;
|
||||||
|
result.IsSuccess = userRes;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.UserAllereadyExist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByNameAsync($"Service-{req.ServiceName}");
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.PrivilageResults = await serviceManager.CreateServicesPrivilage(req.ServicePrivilages);
|
||||||
|
result.RoleResults = await serviceManager.CreateServiceRoles(req.ServiceRoles);
|
||||||
|
result.ServiceToken =
|
||||||
|
await tokenRepo.CreateJwtToken(user, null, new List<Claim>() { AccountType.Service.Claim() });
|
||||||
|
|
||||||
|
var topic = new RegisterNewServiceMessageType()
|
||||||
|
{
|
||||||
|
ServiceName = req.ServiceName,
|
||||||
|
ServiceAccountUserName = user.UserName,
|
||||||
|
ServicePrivilage = req.ServicePrivilages
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = serviceTopic.Produce(topic);
|
||||||
|
logger.LogInformation($"New service {req.ServiceName} with service account {user.UserName} " +
|
||||||
|
$"and privileges {req.ServicePrivilages} register success.");
|
||||||
|
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HasPrivilege(GlobalPrivilage.Privilege.GlobalAdmin)]
|
||||||
|
public async Task<ActionResult<bool>> TestServiceToken()
|
||||||
|
{
|
||||||
|
var userId = HttpContext.User.Claims.FirstOrDefault(q => q.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (userId == null) return BadRequest();
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId);
|
||||||
|
|
||||||
|
if (user == null) return Ok(false);
|
||||||
|
|
||||||
|
var userClaim = await userManager.GetClaimsAsync(user);
|
||||||
|
|
||||||
|
var serviceClaim = AccountType.Service.Claim();
|
||||||
|
|
||||||
|
if(! userClaim.Any(q=> q.Type ==serviceClaim.Type && q.Value == serviceClaim.Value))
|
||||||
|
{
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Regenerated token for service account: {user.UserName}");
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using OpenWarehouse.auth.lib.Model.Service;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Controller;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("[action]")]
|
||||||
|
public class Tools(
|
||||||
|
ILogger<Tools> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public Task<ActionResult<PingModel>> Ping()
|
||||||
|
{
|
||||||
|
logger.LogInformation("Ping request received.");
|
||||||
|
return Task.FromResult<ActionResult<PingModel>>(Ok(new PingModel()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public Task<ActionResult<bool>> TestAuthStatus()
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Test auth status request received for user: {User.Identity?.Name ?? "Unknown"}.");
|
||||||
|
return Task.FromResult<ActionResult<bool>>(User.Identity?.IsAuthenticated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using OpenWarehouse.auth.lib.Model.WellKnow;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Controller;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
public class WellKnown(IConfiguration configuration, ILogger<WellKnown> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet(".well-known/openid-configuration")]
|
||||||
|
public ActionResult<OpenIdConfiguration> GetOpenIdConfiguration()
|
||||||
|
{
|
||||||
|
var issuer = configuration["Jwt:Issuer"] ?? "https://your-oidc-server.com";
|
||||||
|
var oidcConfig = new OpenIdConfiguration
|
||||||
|
{
|
||||||
|
Issuer = issuer,
|
||||||
|
AuthorizationEndpoint = $"{issuer}/auth/login",
|
||||||
|
TokenEndpoint = $"{issuer}/auth/token",
|
||||||
|
JwksUri = $"{issuer}/.well-known/jwks",
|
||||||
|
ResponseTypesSupported = new[] { "code", "id_token", "token" },
|
||||||
|
SubjectTypesSupported = new[] { "public" },
|
||||||
|
IdTokenSigningAlgValuesSupported = new[] { "RS256" }
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.LogInformation("Served OpenID configuration.");
|
||||||
|
return Ok(oidcConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet(".well-known/jwks")]
|
||||||
|
public ActionResult<JwksResponse> GetJwks()
|
||||||
|
{
|
||||||
|
var rsaKeyPath = Path.Combine(configuration["contentRoot"] ?? throw new Exception("content root not found"), "private_key_pkcs8.pem");
|
||||||
|
|
||||||
|
if (!System.IO.File.Exists(rsaKeyPath))
|
||||||
|
{
|
||||||
|
logger.LogError("RSA key file not found at path: {Path}", rsaKeyPath);
|
||||||
|
return Problem("RSA key not found.", statusCode: 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var rsa = RSA.Create();
|
||||||
|
rsa.ImportFromPem(System.IO.File.ReadAllText(rsaKeyPath));
|
||||||
|
|
||||||
|
var parameters = rsa.ExportParameters(false);
|
||||||
|
var jwk = new Jwk
|
||||||
|
{
|
||||||
|
Kty = "RSA",
|
||||||
|
E = Base64UrlEncoder.Encode(parameters.Exponent),
|
||||||
|
N = Base64UrlEncoder.Encode(parameters.Modulus),
|
||||||
|
Use = "sig",
|
||||||
|
Alg = "RS256",
|
||||||
|
Kid = "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
var jwksResponse = new JwksResponse
|
||||||
|
{
|
||||||
|
Keys = new[] { jwk }
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.LogInformation("JWKS configuration served.");
|
||||||
|
return Ok(jwksResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Data
|
||||||
|
{
|
||||||
|
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>, IDataProtectionKeyContext
|
||||||
|
{
|
||||||
|
|
||||||
|
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual DbSet<ApplicationUser> ApplicationUsers { get; set; }
|
||||||
|
public virtual DbSet<ApplicationRole> ApplicationRoles { get; set; }
|
||||||
|
public DbSet<ApplicationUserTokenRepository> ApplicationUserTokenRepositories { get; set; }
|
||||||
|
public DbSet<RegisterToken> RegisterTokens { get; set; }
|
||||||
|
public DbSet<RolePrivilage> RolePrivilages { get; set; }
|
||||||
|
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Data;
|
||||||
|
|
||||||
|
public class ApplicationDbFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
||||||
|
{
|
||||||
|
public ApplicationDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
IConfigurationRoot configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||||
|
|
||||||
|
// Use the connection string from configuration
|
||||||
|
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||||
|
optionsBuilder.UseNpgsql(connectionString);
|
||||||
|
|
||||||
|
return new ApplicationDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Data;
|
||||||
|
|
||||||
|
public class ApplicationRole : IdentityRole
|
||||||
|
{
|
||||||
|
public ApplicationRole(bool isGlobalAdmin = false) : base()
|
||||||
|
{
|
||||||
|
IsGlobalAdmin = isGlobalAdmin;
|
||||||
|
}
|
||||||
|
public ApplicationRole(string roleName, bool isGlobalAdmin = false) : base(roleName)
|
||||||
|
{
|
||||||
|
IsGlobalAdmin = isGlobalAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RolePrivilage?> Privilege { get; set; } = new();
|
||||||
|
|
||||||
|
public List<RegisterToken>? TokenToCreateUserWithThisRole { get; set; } = new();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public bool IsGlobalAdmin { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Data;
|
||||||
|
|
||||||
|
public class ApplicationUser : IdentityUser
|
||||||
|
{
|
||||||
|
public ApplicationUser() : base()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApplicationUser(string username) : base(username)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[MinLength(2), MaxLength(3)]
|
||||||
|
public string? DialCode { get; set; }
|
||||||
|
|
||||||
|
public List<ApplicationUserTokenRepository> UserToken { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Data;
|
||||||
|
|
||||||
|
public class ApplicationUserTokenRepository
|
||||||
|
{
|
||||||
|
public ApplicationUserTokenRepository()
|
||||||
|
{
|
||||||
|
IsLocked = false;
|
||||||
|
}
|
||||||
|
public ApplicationUserTokenRepository(string? guid,ApplicationUser? user ,DateTime? createdTime, DateTime? expireTime) : this()
|
||||||
|
{
|
||||||
|
this.Creator = user;
|
||||||
|
this.GuidToken = guid;
|
||||||
|
this.CreatedTime = createdTime;
|
||||||
|
ExpireTime = expireTime;
|
||||||
|
}
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
[Required]
|
||||||
|
public ApplicationUser? Creator { get; set; }
|
||||||
|
[Required, MaxLength(37)]
|
||||||
|
public string? GuidToken { get; set; }
|
||||||
|
[Required]
|
||||||
|
public DateTime? CreatedTime { get; set; }
|
||||||
|
public DateTime? ExpireTime { get; set; }
|
||||||
|
[Required]
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool IsLocked { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Data;
|
||||||
|
|
||||||
|
[Index(nameof(Token), IsUnique = true)]
|
||||||
|
public class RegisterToken
|
||||||
|
{
|
||||||
|
public RegisterToken()
|
||||||
|
{
|
||||||
|
IsUsed = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
[Required]
|
||||||
|
public string? Token { get; set; }
|
||||||
|
|
||||||
|
public ApplicationUser? Creator { get; set; }
|
||||||
|
public List<ApplicationRole>? AvaliableRole { get; set; }
|
||||||
|
[DefaultValue(false)]
|
||||||
|
[Required]
|
||||||
|
public bool IsUsed { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Data;
|
||||||
|
|
||||||
|
[Index(nameof(Privilage), IsUnique = true)]
|
||||||
|
public class RolePrivilage()
|
||||||
|
{
|
||||||
|
public RolePrivilage(string privilage) : this()
|
||||||
|
{
|
||||||
|
this.Privilage = privilage;
|
||||||
|
}
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[Required, MaxLength(40)]
|
||||||
|
public string? Privilage { get; set; }
|
||||||
|
public List<ApplicationRole>? Roles { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["OpenWarehouse.auth.api/OpenWarehouse.auth.api.csproj", "OpenWarehouse.auth.api/"]
|
||||||
|
COPY ["OpenWarehouse.auth.lib/OpenWarehouse.auth.lib.csproj", "OpenWarehouse.auth.lib/"]
|
||||||
|
COPY ["OpenWarehouse.Common.Lib/OpenWarehouse.Common.Lib.csproj", "OpenWarehouse.Common.Lib/"]
|
||||||
|
COPY ["Kafka.Lib/Kafka.Lib.csproj", "Kafka.Lib/"]
|
||||||
|
RUN dotnet restore "OpenWarehouse.auth.api/OpenWarehouse.auth.api.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/OpenWarehouse.auth.api"
|
||||||
|
RUN dotnet build "OpenWarehouse.auth.api.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "OpenWarehouse.auth.api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "OpenWarehouse.auth.api.dll"]
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
USER $APP_UID
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
global using System.Security.Claims;
|
||||||
|
global using Microsoft.AspNetCore.Identity;
|
||||||
|
global using Microsoft.AspNetCore.Mvc;
|
||||||
|
global using Microsoft.EntityFrameworkCore;
|
||||||
|
global using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
global using OpenWarehouse.auth.api.Common.PrivilageManager;
|
||||||
|
global using OpenWarehouse.auth.api.Data;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<RootNamespace>OpenWarehouse.auth.api</RootNamespace>
|
||||||
|
<UserSecretsId>87a87599-2a49-46ec-b164-0b31e5a7556b</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Confluent.Kafka" Version="2.6.0" />
|
||||||
|
<PackageReference Include="Confluent.SchemaRegistry" Version="2.6.0" />
|
||||||
|
<PackageReference Include="MassTransit" Version="8.3.1" />
|
||||||
|
<PackageReference Include="MassTransit.Kafka" Version="8.3.1" />
|
||||||
|
<PackageReference Include="MassTransit.SqlTransport.PostgreSQL" Version="8.3.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="8.2.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\.dockerignore">
|
||||||
|
<Link>.dockerignore</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Kafka.Lib\Kafka.Lib.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenWarehouse.auth.lib\OpenWarehouse.auth.lib.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenWarehouse.Common.Lib\OpenWarehouse.Common.Lib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@OpenWarehouse.auth_HostAddress = http://localhost:5026
|
||||||
|
|
||||||
|
GET {{OpenWarehouse.auth_HostAddress}}/Auth/login
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using Kafka.Lib;
|
||||||
|
using Kafka.Lib.Auth;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using OpenWarehouse.auth.api.Common;
|
||||||
|
using OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
using OpenWarehouse.auth.api.Common.DefaultUserAndRole;
|
||||||
|
using OpenWarehouse.auth.api.Common.RegisterTokenManager;
|
||||||
|
using OpenWarehouse.auth.api.Common.ServiceManager;
|
||||||
|
using OpenWarehouse.auth.api.Services;
|
||||||
|
using OpenWarehouse.Common.Lib;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var logger = builder.GetBuildLogger();
|
||||||
|
|
||||||
|
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
|
||||||
|
{
|
||||||
|
options.SignIn.RequireConfirmedAccount = false;
|
||||||
|
options.User.RequireUniqueEmail = true;
|
||||||
|
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||||
|
options.Lockout.AllowedForNewUsers = false;
|
||||||
|
})
|
||||||
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
|
||||||
|
var kafkaConfig = builder.Configuration.GetSection("Kafka").Get<KafkaConfig>();
|
||||||
|
|
||||||
|
builder.AddAuthenticationService();
|
||||||
|
|
||||||
|
var connectionString = Environment.GetEnvironmentVariable("ConnectionString") ??
|
||||||
|
builder.Configuration.GetConnectionString("DefaultConnection");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(connectionString))
|
||||||
|
{
|
||||||
|
logger.LogError("Connection string 'DefaultConnection' not found");
|
||||||
|
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
options.UseNpgsql(connectionString));
|
||||||
|
|
||||||
|
builder.Services.AddDataProtection()
|
||||||
|
.PersistKeysToDbContext<ApplicationDbContext>();
|
||||||
|
|
||||||
|
|
||||||
|
builder.Host
|
||||||
|
.UseMassTransit((hostContext, configurator) =>
|
||||||
|
{
|
||||||
|
configurator.UsingInMemory();
|
||||||
|
|
||||||
|
configurator.AddRider(r =>
|
||||||
|
{
|
||||||
|
var warehouseTopicName = $"events.OpenWarehouse.{kafkaConfig?.TopicRoot}";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
r.AddProducer<LogOutMessageType>($"{warehouseTopicName}-warehouse-topicName-logout");
|
||||||
|
r.AddProducer<PermisionChangeMassageType>(warehouseTopicName+".PermisionChange");
|
||||||
|
r.AddProducer<RegisterNewServiceMessageType>(warehouseTopicName+".RegisterNewServic");
|
||||||
|
r.AddProducer<TokenValidationMessageType>(warehouseTopicName+".TokenValidation");
|
||||||
|
r.AddProducer<UserChangeClaimsMessageType>(warehouseTopicName+".UserClaimChange");
|
||||||
|
r.AddProducer<UserChangeMessageType>(warehouseTopicName+".UserChange");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError("Error configuring Kafka producers: {Message}", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.UsingKafka((context, cfg) =>
|
||||||
|
{
|
||||||
|
cfg.Host(kafkaConfig?.BootstrapServers);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddMassTransitHostedService();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Please insert JWT with Bearer into field",
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
Scheme = "Bearer"
|
||||||
|
});
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
|
||||||
|
},
|
||||||
|
Array.Empty<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
options.AddServer(new OpenApiServer { Url = "/auth/" });
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<AuthTokenRepo>();
|
||||||
|
builder.Services.AddScoped<PrivilegeManager>();
|
||||||
|
builder.Services.AddScoped<RegisterTokenManager>();
|
||||||
|
builder.Services.AddScoped<DefaultIdentify>();
|
||||||
|
builder.Services.AddScoped<IServiceManager, ServiceManager>();
|
||||||
|
|
||||||
|
builder.Services.AddControllers(options =>
|
||||||
|
{
|
||||||
|
options.Filters.Add(new IgnoreAntiforgeryTokenAttribute());
|
||||||
|
})
|
||||||
|
.ConfigureApiBehaviorOptions(options =>
|
||||||
|
{
|
||||||
|
options.SuppressConsumesConstraintForFormFileParameters = true;
|
||||||
|
options.SuppressInferBindingSourcesForParameters = true;
|
||||||
|
options.SuppressModelStateInvalidFilter = true;
|
||||||
|
options.SuppressMapClientErrors = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
var context = services.GetRequiredService<ApplicationDbContext>();
|
||||||
|
context.Database.Migrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
var seedService = services.GetRequiredService<DefaultIdentify>();
|
||||||
|
await seedService.SeedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Disable to connection in docker network
|
||||||
|
// app.UseHttpsRedirection();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:62345",
|
||||||
|
"sslPort": 44336
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5026",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7291;http://localhost:5026",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using OpenWarehouse.auth.api.Common.AuthTokenRepo;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.api.Services;
|
||||||
|
|
||||||
|
public static class AuthenticationConfigService
|
||||||
|
{
|
||||||
|
public static T AddAuthenticationService<T>(this T builder)
|
||||||
|
where T : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
var privateKeyPath = Path.Combine(builder.Environment.ContentRootPath, "private_key_pkcs8.pem");
|
||||||
|
var rsa = RSA.Create();
|
||||||
|
|
||||||
|
if (!File.Exists(privateKeyPath))
|
||||||
|
{
|
||||||
|
rsa = RSA.Create(2048);
|
||||||
|
var privateKeyPem = ExportPrivateKeyToPem(rsa);
|
||||||
|
File.WriteAllText(privateKeyPath, privateKeyPem);
|
||||||
|
Console.WriteLine($"Generated new RSA private key at: {privateKeyPath}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var privateKey = File.ReadAllText(privateKeyPath);
|
||||||
|
rsa.ImportFromPem(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rsaPublic = rsa.ExportParameters(false);
|
||||||
|
var rsaSecurityKey = new RsaSecurityKey(rsa)
|
||||||
|
{
|
||||||
|
KeyId = "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configure JWT Bearer authentication
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = rsaSecurityKey,
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
|
builder.Configuration["Jwt:AesKey"] ?? throw new InvalidOperationException("Missing Jwt:AesKey in configuration"))),
|
||||||
|
ValidAudience = builder.Configuration["Jwt:Audience"],
|
||||||
|
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnTokenValidated = async context =>
|
||||||
|
{
|
||||||
|
var tokenBlacklistService = context.HttpContext.RequestServices.GetRequiredService<AuthTokenRepo>();
|
||||||
|
var token = context.SecurityToken.Id;
|
||||||
|
|
||||||
|
if (await tokenBlacklistService.TokenIsLock(guid: token))
|
||||||
|
{
|
||||||
|
context.Fail("This token has been revoked.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton(new
|
||||||
|
{
|
||||||
|
keys = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
kty = "RSA",
|
||||||
|
e = Base64UrlEncoder.Encode(rsaPublic.Exponent),
|
||||||
|
n = Base64UrlEncoder.Encode(rsaPublic.Modulus),
|
||||||
|
use = "sig",
|
||||||
|
alg = "RS256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExportPrivateKeyToPem(RSA rsa)
|
||||||
|
{
|
||||||
|
var privateKeyBytes = rsa.ExportPkcs8PrivateKey();
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("-----BEGIN PRIVATE KEY-----");
|
||||||
|
builder.AppendLine(Convert.ToBase64String(privateKeyBytes, Base64FormattingOptions.InsertLineBreaks));
|
||||||
|
builder.AppendLine("-----END PRIVATE KEY-----");
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.lib;
|
||||||
|
|
||||||
|
public class AuthTokenHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private string _token;
|
||||||
|
|
||||||
|
public AuthTokenHandler(string token)
|
||||||
|
{
|
||||||
|
_token = token;
|
||||||
|
InnerHandler = new HttpClientHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateToken(string newToken)
|
||||||
|
{
|
||||||
|
_token = newToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_token))
|
||||||
|
{
|
||||||
|
if (_token.Contains(" "))
|
||||||
|
{
|
||||||
|
var tokenSplit = _token.Split(" ");
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue(tokenSplit[0], tokenSplit[1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class AccountDetailRespond
|
||||||
|
{
|
||||||
|
public string? Id { get; set; }
|
||||||
|
public string? DialCode { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, string> Problems { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class AccountListsRequest
|
||||||
|
{
|
||||||
|
public int Page { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class AccountListsResponse : AccountListsRequest
|
||||||
|
{
|
||||||
|
private List<AccountDetailRespond> _accountDetailList = new();
|
||||||
|
|
||||||
|
public bool NextPageExist { get; set; }
|
||||||
|
public int LastPageNum { get; set; }
|
||||||
|
|
||||||
|
public List<AccountDetailRespond> AccountsDetail
|
||||||
|
{
|
||||||
|
get => _accountDetailList;
|
||||||
|
set => _accountDetailList = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class AddClaimsToUserModel
|
||||||
|
{
|
||||||
|
private Dictionary<string, string> _newClaims = new();
|
||||||
|
private string _userId = "";
|
||||||
|
private string _issuer = "";
|
||||||
|
|
||||||
|
public string UserId
|
||||||
|
{
|
||||||
|
get => _userId;
|
||||||
|
set => _userId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Issuer
|
||||||
|
{
|
||||||
|
get => _issuer;
|
||||||
|
set => _issuer = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> NewClaims
|
||||||
|
{
|
||||||
|
get => _newClaims;
|
||||||
|
set => _newClaims = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class AddClaimsToUserReturnModel
|
||||||
|
{
|
||||||
|
private Dictionary<string, string> _allUserClaims = new Dictionary<string, string>();
|
||||||
|
private string? _error = "";
|
||||||
|
private bool _isSucess = false;
|
||||||
|
private string? _userId = "";
|
||||||
|
|
||||||
|
public string? UserId
|
||||||
|
{
|
||||||
|
get => _userId;
|
||||||
|
set => _userId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSucess
|
||||||
|
{
|
||||||
|
get => _isSucess;
|
||||||
|
set => _isSucess = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Error
|
||||||
|
{
|
||||||
|
get => _error;
|
||||||
|
set => _error = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> AllUserClaims
|
||||||
|
{
|
||||||
|
get => _allUserClaims;
|
||||||
|
set => _allUserClaims = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class ChangeEmailRequest
|
||||||
|
{
|
||||||
|
public string? NewEmail { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class ChangeEmailRespond : ChangeEmailRequest
|
||||||
|
{
|
||||||
|
public string? OldEmail { get; set; }
|
||||||
|
public bool ValSuccess { get; set; } = false;
|
||||||
|
public bool UpdateSuccess { get; set; } = false;
|
||||||
|
public Dictionary<string, string> Problems { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class ChangePassword
|
||||||
|
{
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string? NewPassword1 { get; set; }
|
||||||
|
public string? NewPassword2 { get; set; }
|
||||||
|
public bool LogOutAllSession { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class ChangePasswordReturn
|
||||||
|
{
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
public string? Mess { get; set; }
|
||||||
|
public int LockedTokenCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class ChangePhoneNumberRequest
|
||||||
|
{
|
||||||
|
public string? Prefix { get; set; }
|
||||||
|
public string? Number { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class ChangePhoneNumberRespond : ChangePhoneNumberRequest
|
||||||
|
{
|
||||||
|
public string? OldPrefix { get; set; }
|
||||||
|
public string? OldPhone { get; set; }
|
||||||
|
|
||||||
|
public bool ValSuccess { get; set; } = false;
|
||||||
|
public bool UpdateSuccess { get; set; } = false;
|
||||||
|
public Dictionary<string, string> Problems { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class CreateUserModel()
|
||||||
|
{
|
||||||
|
public string? Username { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Dialcode { get; set; }
|
||||||
|
public List<string>? Roles { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class CreateUserModelReturn() : CreateUserModel()
|
||||||
|
{
|
||||||
|
public CreateUserModelReturn(string? returnMess, string? newUserId, bool isCreated, List<RoleStatus>? roleStatusList) : this()
|
||||||
|
{
|
||||||
|
ReturnMess = returnMess;
|
||||||
|
NewUserId = newUserId;
|
||||||
|
IsCreated = isCreated;
|
||||||
|
Roles = roleStatusList;
|
||||||
|
}
|
||||||
|
public string? ReturnMess { get; set; }
|
||||||
|
public string? NewUserId { get; set; }
|
||||||
|
public bool IsCreated { get; set; }
|
||||||
|
public new List<RoleStatus>? Roles { get; set; }
|
||||||
|
|
||||||
|
public class RoleStatus
|
||||||
|
{
|
||||||
|
public bool Exist { get; set; }
|
||||||
|
public bool Added { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Mess { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class DeactiveTokenModel
|
||||||
|
{
|
||||||
|
public string? Token { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class DeleteRoleModel
|
||||||
|
{
|
||||||
|
public string? RoleIdentify { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class DeleteUserRequestModel
|
||||||
|
{
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class DeleteUserReturnModel : DeleteUserRequestModel
|
||||||
|
{
|
||||||
|
public string? OperationMessage { get; set; }
|
||||||
|
public bool IsOperationSuccess { get; set; }
|
||||||
|
public int DedeltedTokenCount { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class EditAccountRequest
|
||||||
|
{
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? NewUserName { get; set; }
|
||||||
|
public string? NewEmail { get; set; }
|
||||||
|
public string? NewDialCode { get; set; }
|
||||||
|
public string? NewPhone { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class EditAccountRespond : EditAccountRequest
|
||||||
|
{
|
||||||
|
[DefaultValue(false)] public bool UserNameEdited { get; set; } = false;
|
||||||
|
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool EmailEdited { get; set; } = false;
|
||||||
|
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool PhoneEdited { get; set; } = false;
|
||||||
|
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool Success { get; set; } = false;
|
||||||
|
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Account;
|
||||||
|
|
||||||
|
public class LoginModel
|
||||||
|
{
|
||||||
|
public LoginModel()
|
||||||
|
{
|
||||||
|
IsLogged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Login { set; get; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Token { set; get; }
|
||||||
|
public long Exp { get; set; } = 0;
|
||||||
|
public string? Message { set; get; }
|
||||||
|
public string? AccountProblem { set; get; }
|
||||||
|
public bool IsLogged { get; set; }
|
||||||
|
public bool RequiresTwoFactor { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
public class LogOutModel
|
||||||
|
{
|
||||||
|
public LogOutModel(bool success = false, string? mess = null)
|
||||||
|
{
|
||||||
|
LogOutSuccess = success;
|
||||||
|
Mess = mess;
|
||||||
|
}
|
||||||
|
public bool LogOutSuccess { get; set; }
|
||||||
|
public string? Mess { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
public class LoginRequestModel
|
||||||
|
{
|
||||||
|
[Display(Name = "Username")]
|
||||||
|
[Required(ErrorMessage = "Username is required")]
|
||||||
|
public string? Login { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Password")]
|
||||||
|
[Required(ErrorMessage = "Password is required")]
|
||||||
|
public string? Pass { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
public class RegenerateTokenModel()
|
||||||
|
{
|
||||||
|
public RegenerateTokenModel(string token) : this()
|
||||||
|
{
|
||||||
|
this.Token = token;
|
||||||
|
}
|
||||||
|
public string? Token { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
public class RegeneratedTokenModel() : RegenerateTokenModel()
|
||||||
|
{
|
||||||
|
public RegeneratedTokenModel(string Token, string mess = "") : this()
|
||||||
|
{
|
||||||
|
this.Token = Token;
|
||||||
|
this.Mess = mess;
|
||||||
|
|
||||||
|
}
|
||||||
|
public string? Mess { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
public class RegisterModel
|
||||||
|
{
|
||||||
|
public string? RegisterToken { get; set; }
|
||||||
|
public string? Username { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string[]? Roles { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
public class RegisterReturnModel
|
||||||
|
{
|
||||||
|
public bool IsCreated { get; set; }
|
||||||
|
public string? AuthToken { get; set; }
|
||||||
|
public string? ProblemWithUsername { get; set; }
|
||||||
|
public string? ProblemWitbEmail { get; set; }
|
||||||
|
public string? ProblemWithPassword { get; set; }
|
||||||
|
public string? ProblemWithToken { get; set; }
|
||||||
|
public string? OtherWithOtherProblem { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Auth;
|
||||||
|
|
||||||
|
public class TokenAskIfIsLockModel
|
||||||
|
{
|
||||||
|
public string? Token { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model;
|
||||||
|
|
||||||
|
public class ErrorModel
|
||||||
|
{
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model;
|
||||||
|
|
||||||
|
public class OkModel()
|
||||||
|
{
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public object? Value { get; set; }
|
||||||
|
|
||||||
|
public OkModel(string message, bool isSuccess = true, object? value = null) : this()
|
||||||
|
{
|
||||||
|
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
Message = message;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Privilage;
|
||||||
|
|
||||||
|
public class CreatePrivilageModel
|
||||||
|
{
|
||||||
|
public string? Privilage { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using NJsonSchema.Annotations;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.lib.Model.Role;
|
||||||
|
|
||||||
|
public class AddRoleToUserModel
|
||||||
|
{
|
||||||
|
[NotNull, MinLength(1), MaxLength(40)]
|
||||||
|
public string? UserIdentify { get; set; }
|
||||||
|
[NotNull, MinLength(1), MaxLength(40)]
|
||||||
|
public string? RoleIdentify { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Role;
|
||||||
|
|
||||||
|
public class CreateRoleModel
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public List<string>? Privilage { get; set; }
|
||||||
|
public bool ForcePrivilage { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace OpenWarehouse.auth.lib.Model.Role;
|
||||||
|
|
||||||
|
public class EditRoleModel
|
||||||
|
{
|
||||||
|
public string? RoleIdentify { get; set; }
|
||||||
|
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool ForceAddedPrivilage { get; set; } = false;
|
||||||
|
|
||||||
|
public List<string>? AddPrivilage { get; set; }
|
||||||
|
public List<string>? DeletePrivilage { get; set; }
|
||||||
|
public string? NewName { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace OpenWarehouse.auth.lib.Model.Role;
|
||||||
|
|
||||||
|
public class RoleListModel
|
||||||
|
{
|
||||||
|
public RoleListModel()
|
||||||
|
{
|
||||||
|
Roles = new List<RoleModel>();
|
||||||
|
}
|
||||||
|
public RoleListModel(List<RoleModel>? roles) :this()
|
||||||
|
{
|
||||||
|
Roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RoleModel>? Roles { get; set; }
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user