Merge pull request #282 from hchen2020/master
Improve UserIdentity implementation.
This commit is contained in:
commit
44c29a2afe
|
|
@ -18,6 +18,7 @@ public interface IBotSharpRepository
|
|||
#region User
|
||||
User? GetUserByEmail(string email);
|
||||
User? GetUserById(string id);
|
||||
User? GetUserByUserName(string userName);
|
||||
void CreateUser(User user);
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using BotSharp.Abstraction.Users.Models;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BotSharp.Abstraction.Users;
|
||||
|
||||
public interface IAuthenticationHook
|
||||
{
|
||||
Task<User> Authenticate(string id, string password);
|
||||
void AddClaims(List<Claim> claims);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ public interface IUserIdentity
|
|||
string UserName { get; }
|
||||
string FirstName { get; }
|
||||
string LastName { get; }
|
||||
string FullName { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,20 +192,17 @@ public class BotSharpDbContext : Database, IBotSharpRepository
|
|||
#endregion
|
||||
|
||||
#region User
|
||||
public User? GetUserByEmail(string email)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public User? GetUserByEmail(string email)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public User? GetUserById(string id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public User? GetUserById(string id)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void CreateUser(User user)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public User? GetUserByUserName(string userName)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void CreateUser(User user)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
using BotSharp.Abstraction.Users.Models;
|
||||
using System.IO;
|
||||
|
||||
namespace BotSharp.Core.Repository
|
||||
namespace BotSharp.Core.Repository;
|
||||
|
||||
public partial class FileRepository
|
||||
{
|
||||
public partial class FileRepository
|
||||
public User? GetUserByEmail(string email)
|
||||
{
|
||||
public User? GetUserByEmail(string email)
|
||||
{
|
||||
return Users.FirstOrDefault(x => x.Email == email);
|
||||
}
|
||||
return Users.FirstOrDefault(x => x.Email == email.ToLower());
|
||||
}
|
||||
|
||||
public User? GetUserById(string id = null)
|
||||
{
|
||||
return Users.FirstOrDefault(x => x.ExternalId == id || x.Id == id);
|
||||
}
|
||||
public User? GetUserById(string id = null)
|
||||
{
|
||||
return Users.FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id));
|
||||
}
|
||||
|
||||
public void CreateUser(User user)
|
||||
public User? GetUserByUserName(string userName = null)
|
||||
{
|
||||
return Users.FirstOrDefault(x => x.UserName == userName.ToLower());
|
||||
}
|
||||
|
||||
public void CreateUser(User user)
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
user.Id = userId;
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, "users", userId);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
user.Id = userId;
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, "users", userId);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
var path = Path.Combine(dir, "user.json");
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(user, _options));
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
var path = Path.Combine(dir, "user.json");
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(user, _options));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ using FunctionDef = BotSharp.Abstraction.Functions.Models.FunctionDef;
|
|||
using BotSharp.Abstraction.Users.Models;
|
||||
using BotSharp.Abstraction.Agents.Models;
|
||||
using MongoDB.Driver;
|
||||
using BotSharp.Abstraction.Routing.Models;
|
||||
using BotSharp.Abstraction.Repositories.Filters;
|
||||
using BotSharp.Abstraction.Repositories.Models;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
using BotSharp.Abstraction.Evaluations.Settings;
|
||||
using System.Text.Encodings.Web;
|
||||
using BotSharp.Abstraction.Plugins.Models;
|
||||
using BotSharp.Abstraction.Statistics.Settings;
|
||||
|
|
|
|||
|
|
@ -28,4 +28,7 @@ public class UserIdentity : IUserIdentity
|
|||
|
||||
public string LastName
|
||||
=> _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value!;
|
||||
|
||||
public string FullName
|
||||
=> $"{FirstName} {LastName}".Trim();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,34 +12,43 @@ public class UserService : IUserService
|
|||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IUserIdentity _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public UserService(IServiceProvider services, IUserIdentity user)
|
||||
public UserService(IServiceProvider services, IUserIdentity user, ILogger<UserService> logger)
|
||||
{
|
||||
_services = services;
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<User> CreateUser(User user)
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.UserName))
|
||||
{
|
||||
// generate unique name
|
||||
var name = user.Email.Split("@").First() + "-" + Nanoid.Generate("0123456789botsharp", 6);
|
||||
user.UserName = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
user.UserName = user.UserName.ToLower();
|
||||
}
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetUserByEmail(user.Email);
|
||||
var record = db.GetUserByUserName(user.UserName);
|
||||
|
||||
if (record != null)
|
||||
{
|
||||
return record;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(user.Id))
|
||||
{
|
||||
user.Id = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
record = user;
|
||||
|
||||
record.Email = user.Email.ToLower();
|
||||
if (string.IsNullOrEmpty(user.UserName))
|
||||
{
|
||||
var name = record.Email.Split("@").First() + "-" + Nanoid.Generate("123456789botsharp", 6);
|
||||
record.UserName = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
record.UserName = user.UserName.ToLower();
|
||||
}
|
||||
record.Salt = Guid.NewGuid().ToString("N");
|
||||
record.Password = Utilities.HashText(user.Password, record.Salt);
|
||||
|
||||
|
|
@ -53,18 +62,34 @@ public class UserService : IUserService
|
|||
public async Task<Token> GetToken(string authorization)
|
||||
{
|
||||
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
|
||||
var (userEmail, password) = base64.SplitAsTuple(":");
|
||||
var (id, password) = base64.SplitAsTuple(":");
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetUserByEmail(userEmail);
|
||||
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
|
||||
if (record == null)
|
||||
{
|
||||
record = db.GetUserByUserName(id);
|
||||
}
|
||||
|
||||
if (record == null || record.Source != "internal")
|
||||
{
|
||||
// check 3rd party user
|
||||
var validators = _services.GetServices<IAuthenticationHook>();
|
||||
foreach (var validator in validators)
|
||||
{
|
||||
var user = await validator.Authenticate(userEmail, password);
|
||||
if (user != null)
|
||||
var user = await validator.Authenticate(id, password);
|
||||
if (user == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(user.Source) || user.Source == "internal")
|
||||
{
|
||||
_logger.LogError($"Please set source name in the Authenticate hook.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (record == null)
|
||||
{
|
||||
// create a local user record
|
||||
record = new User
|
||||
|
|
@ -74,11 +99,12 @@ public class UserService : IUserService
|
|||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Source = user.Source,
|
||||
ExternalId = user.ExternalId
|
||||
ExternalId = user.ExternalId,
|
||||
Password = user.Password,
|
||||
};
|
||||
await CreateUser(record);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,26 +131,36 @@ public class UserService : IUserService
|
|||
|
||||
private string GenerateJwtToken(User user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.NameId, user.Id),
|
||||
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName),
|
||||
new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName),
|
||||
new Claim("source", user.Source),
|
||||
new Claim("external_id", user.ExternalId),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var validators = _services.GetServices<IAuthenticationHook>();
|
||||
foreach (var validator in validators)
|
||||
{
|
||||
validator.AddClaims(claims);
|
||||
}
|
||||
|
||||
var config = _services.GetRequiredService<IConfiguration>();
|
||||
var issuer = config["Jwt:Issuer"];
|
||||
var audience = config["Jwt:Audience"];
|
||||
var key = Encoding.ASCII.GetBytes(config["Jwt:Key"]);
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.NameId, user.Id),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName),
|
||||
new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
}),
|
||||
Expires = DateTime.UtcNow.AddMinutes(5),
|
||||
Subject = new ClaimsIdentity(claims),
|
||||
Expires = DateTime.UtcNow.AddHours(2),
|
||||
Issuer = issuer,
|
||||
Audience = audience,
|
||||
SigningCredentials = new SigningCredentials
|
||||
(new SymmetricSecurityKey(key),
|
||||
SecurityAlgorithms.HmacSha512Signature)
|
||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
|
||||
SecurityAlgorithms.HmacSha512Signature)
|
||||
};
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
|
|
@ -144,6 +180,16 @@ public class UserService : IUserService
|
|||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var user = db.GetUserById(id);
|
||||
if (user == null)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Id = id,
|
||||
FirstName = "Unknown",
|
||||
LastName = "Anonymous",
|
||||
Role = AgentRole.User
|
||||
};
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public class AgentController : ControllerBase
|
|||
{
|
||||
AgentIds = new List<string> { id }
|
||||
});
|
||||
return agents.Items.First();
|
||||
return agents.Items.FirstOrDefault();
|
||||
}
|
||||
|
||||
[HttpGet("/agents")]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using BotSharp.Abstraction.Users.Models;
|
||||
|
||||
namespace BotSharp.OpenAPI.Controllers;
|
||||
|
||||
[Authorize]
|
||||
|
|
@ -70,16 +72,29 @@ public class ConversationController : ControllerBase
|
|||
var dialogs = new List<ChatResponseModel>();
|
||||
foreach (var message in history)
|
||||
{
|
||||
var user = await userService.GetUser(message.SenderId);
|
||||
|
||||
dialogs.Add(new ChatResponseModel
|
||||
if (message.Role == AgentRole.User)
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
Text = message.Content,
|
||||
Sender = UserViewModel.FromUser(user)
|
||||
});
|
||||
var user = await userService.GetUser(message.SenderId);
|
||||
|
||||
dialogs.Add(new ChatResponseModel
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
Text = message.Content,
|
||||
Sender = UserViewModel.FromUser(user)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
dialogs.Add(new ChatResponseModel
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
Text = message.Content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dialogs;
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ public class UserViewModel
|
|||
{
|
||||
return new UserViewModel
|
||||
{
|
||||
FirstName = "AI",
|
||||
LastName = "Assistant",
|
||||
Role = AgentRole.Assistant
|
||||
FirstName = "Unknown",
|
||||
LastName = "Anonymous",
|
||||
Role = AgentRole.User
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using BotSharp.Abstraction.Users.Models;
|
||||
|
||||
namespace BotSharp.Plugin.MongoStorage.Collections;
|
||||
|
||||
public class UserDocument : MongoBase
|
||||
|
|
@ -14,4 +16,21 @@ public class UserDocument : MongoBase
|
|||
|
||||
public DateTime CreatedTime { get; set; }
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
|
||||
public User ToUser()
|
||||
{
|
||||
return new User
|
||||
{
|
||||
Id = Id,
|
||||
UserName = UserName,
|
||||
FirstName = FirstName,
|
||||
LastName = LastName,
|
||||
Email = Email,
|
||||
Password = Password,
|
||||
Salt = Salt,
|
||||
Source = Source,
|
||||
ExternalId = ExternalId,
|
||||
Role = Role
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -7,38 +7,21 @@ public partial class MongoRepository
|
|||
{
|
||||
public User? GetUserByEmail(string email)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Email == email);
|
||||
return user != null ? new User
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Email = user.Email,
|
||||
Password = user.Password,
|
||||
Salt = user.Salt,
|
||||
Source = user.Source,
|
||||
ExternalId = user.ExternalId,
|
||||
Role = user.Role
|
||||
} : null;
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Email == email.ToLower());
|
||||
return user != null ? user.ToUser() : null;
|
||||
}
|
||||
|
||||
public User? GetUserById(string id)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Id == id || x.ExternalId == id);
|
||||
return user != null ? new User
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Email = user.Email,
|
||||
Password = user.Password,
|
||||
Salt = user.Salt,
|
||||
Source = user.Source,
|
||||
ExternalId = user.ExternalId,
|
||||
Role = user.Role
|
||||
} : null;
|
||||
var user = _dc.Users.AsQueryable()
|
||||
.FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id));
|
||||
return user != null ? user.ToUser() : null;
|
||||
}
|
||||
|
||||
public User? GetUserByUserName(string userName)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.UserName == userName.ToLower());
|
||||
return user != null ? user.ToUser() : null;
|
||||
}
|
||||
|
||||
public void CreateUser(User user)
|
||||
|
|
@ -47,7 +30,7 @@ public partial class MongoRepository
|
|||
|
||||
var userCollection = new UserDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Id = user.Id ?? Guid.NewGuid().ToString(),
|
||||
UserName = user.UserName,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
|
|
|
|||
Loading…
Reference in a new issue