2023-06-11 23:46:02 +00:00
|
|
|
using BotSharp.Abstraction.Users.Models;
|
2024-05-26 01:34:29 +00:00
|
|
|
using BotSharp.Abstraction.Users.Settings;
|
|
|
|
|
using BotSharp.OpenAPI.ViewModels.Users;
|
2023-06-11 23:46:02 +00:00
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
|
using Microsoft.IdentityModel.Tokens;
|
2024-01-31 15:15:41 +00:00
|
|
|
using NanoidDotNet;
|
2023-06-11 23:46:02 +00:00
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
|
|
|
using System.Security.Claims;
|
2024-05-31 03:24:34 +00:00
|
|
|
using System.Text.RegularExpressions;
|
2023-06-11 23:46:02 +00:00
|
|
|
|
|
|
|
|
namespace BotSharp.Core.Users.Services;
|
|
|
|
|
|
|
|
|
|
public class UserService : IUserService
|
|
|
|
|
{
|
|
|
|
|
private readonly IServiceProvider _services;
|
2023-06-17 02:42:35 +00:00
|
|
|
private readonly IUserIdentity _user;
|
2024-01-31 19:57:54 +00:00
|
|
|
private readonly ILogger _logger;
|
2024-05-26 01:34:29 +00:00
|
|
|
private readonly AccountSetting _setting;
|
2023-06-11 23:46:02 +00:00
|
|
|
|
2024-05-30 09:11:42 +00:00
|
|
|
public UserService(IServiceProvider services,
|
|
|
|
|
IUserIdentity user,
|
2024-05-26 01:34:29 +00:00
|
|
|
ILogger<UserService> logger,
|
|
|
|
|
AccountSetting setting)
|
2023-06-11 23:46:02 +00:00
|
|
|
{
|
|
|
|
|
_services = services;
|
|
|
|
|
_user = user;
|
2024-01-31 19:57:54 +00:00
|
|
|
_logger = logger;
|
2024-05-26 01:34:29 +00:00
|
|
|
_setting = setting;
|
2023-06-11 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<User> CreateUser(User user)
|
|
|
|
|
{
|
2024-01-31 19:57:54 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 04:53:22 +00:00
|
|
|
var db = _services.GetRequiredService<IBotSharpRepository>();
|
2024-01-31 19:57:54 +00:00
|
|
|
var record = db.GetUserByUserName(user.UserName);
|
|
|
|
|
|
2023-06-11 23:46:02 +00:00
|
|
|
if (record != null)
|
|
|
|
|
{
|
2023-09-07 22:04:34 +00:00
|
|
|
return record;
|
2023-06-11 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-31 19:57:54 +00:00
|
|
|
if (string.IsNullOrEmpty(user.Id))
|
2024-01-31 15:15:41 +00:00
|
|
|
{
|
2024-01-31 19:57:54 +00:00
|
|
|
user.Id = Guid.NewGuid().ToString();
|
2024-01-31 15:15:41 +00:00
|
|
|
}
|
2024-01-31 19:57:54 +00:00
|
|
|
|
|
|
|
|
record = user;
|
2024-02-14 23:45:28 +00:00
|
|
|
record.Email = user.Email?.ToLower();
|
2024-05-31 03:24:34 +00:00
|
|
|
if (user.Phone != null)
|
|
|
|
|
{
|
|
|
|
|
record.Phone = "+" + Regex.Match(user.Phone, @"\d+").Value;
|
|
|
|
|
}
|
2023-06-11 23:46:02 +00:00
|
|
|
record.Salt = Guid.NewGuid().ToString("N");
|
|
|
|
|
record.Password = Utilities.HashText(user.Password, record.Salt);
|
|
|
|
|
|
2024-05-26 01:34:29 +00:00
|
|
|
if (_setting.NewUserVerification)
|
|
|
|
|
{
|
|
|
|
|
record.VerificationCode = Nanoid.Generate(alphabet: "0123456789", size: 6);
|
|
|
|
|
record.Verified = false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 05:10:46 +00:00
|
|
|
db.CreateUser(record);
|
2024-01-05 03:20:50 +00:00
|
|
|
|
2024-03-12 16:54:20 +00:00
|
|
|
_logger.LogWarning($"Created new user account: {record.Id} {record.UserName}");
|
2024-01-05 03:20:50 +00:00
|
|
|
Utilities.ClearCache();
|
|
|
|
|
|
2024-05-21 10:51:38 +00:00
|
|
|
var hooks = _services.GetServices<IAuthenticationHook>();
|
|
|
|
|
foreach (var hook in hooks)
|
|
|
|
|
{
|
|
|
|
|
await hook.UserCreated(record);
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-07 22:04:34 +00:00
|
|
|
return record;
|
2023-06-11 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-20 16:35:45 +00:00
|
|
|
public async Task<Token?> GetToken(string authorization)
|
2023-06-11 23:46:02 +00:00
|
|
|
{
|
|
|
|
|
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
|
2024-01-31 19:57:54 +00:00
|
|
|
var (id, password) = base64.SplitAsTuple(":");
|
2023-06-11 23:46:02 +00:00
|
|
|
|
2023-08-10 04:53:22 +00:00
|
|
|
var db = _services.GetRequiredService<IBotSharpRepository>();
|
2024-01-31 19:57:54 +00:00
|
|
|
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
|
2024-01-31 15:15:41 +00:00
|
|
|
if (record == null)
|
2024-01-31 19:57:54 +00:00
|
|
|
{
|
|
|
|
|
record = db.GetUserByUserName(id);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-23 12:05:36 +00:00
|
|
|
User? user = record;
|
2024-06-12 18:40:29 +00:00
|
|
|
var isAuthenticatedByHook = false;
|
2024-02-05 16:00:28 +00:00
|
|
|
var hooks = _services.GetServices<IAuthenticationHook>();
|
2024-05-30 09:11:42 +00:00
|
|
|
if (record == null || record.Source != "internal")
|
2024-01-31 15:15:41 +00:00
|
|
|
{
|
|
|
|
|
// check 3rd party user
|
2024-02-05 16:00:28 +00:00
|
|
|
foreach (var hook in hooks)
|
2024-01-31 15:15:41 +00:00
|
|
|
{
|
2024-05-20 16:35:45 +00:00
|
|
|
user = await hook.Authenticate(id, password);
|
2024-01-31 19:57:54 +00:00
|
|
|
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)
|
2024-01-31 15:15:41 +00:00
|
|
|
{
|
|
|
|
|
// create a local user record
|
|
|
|
|
record = new User
|
|
|
|
|
{
|
|
|
|
|
UserName = user.UserName,
|
|
|
|
|
Email = user.Email,
|
|
|
|
|
FirstName = user.FirstName,
|
|
|
|
|
LastName = user.LastName,
|
|
|
|
|
Source = user.Source,
|
2024-01-31 19:57:54 +00:00
|
|
|
ExternalId = user.ExternalId,
|
|
|
|
|
Password = user.Password,
|
2024-01-31 15:15:41 +00:00
|
|
|
};
|
|
|
|
|
await CreateUser(record);
|
|
|
|
|
}
|
2024-06-12 18:40:29 +00:00
|
|
|
|
|
|
|
|
isAuthenticatedByHook = true;
|
2024-01-31 19:57:54 +00:00
|
|
|
break;
|
2024-01-31 15:15:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-20 16:35:45 +00:00
|
|
|
if ((!hooks.IsNullOrEmpty() && user == null) || record == null)
|
2023-06-11 23:46:02 +00:00
|
|
|
{
|
|
|
|
|
return default;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-12 18:40:29 +00:00
|
|
|
if (!isAuthenticatedByHook && _setting.NewUserVerification && !record.Verified)
|
2024-05-26 01:34:29 +00:00
|
|
|
{
|
|
|
|
|
return default;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-20 14:23:48 +00:00
|
|
|
#if !DEBUG
|
2024-06-12 18:40:29 +00:00
|
|
|
if (!isAuthenticatedByHook && Utilities.HashText(password, record.Salt) != record.Password)
|
2023-06-11 23:46:02 +00:00
|
|
|
{
|
|
|
|
|
return default;
|
|
|
|
|
}
|
2024-03-20 14:23:48 +00:00
|
|
|
#endif
|
2023-06-11 23:46:02 +00:00
|
|
|
|
|
|
|
|
var accessToken = GenerateJwtToken(record);
|
|
|
|
|
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
|
2024-02-05 16:00:28 +00:00
|
|
|
var token = new Token
|
2023-06-11 23:46:02 +00:00
|
|
|
{
|
|
|
|
|
AccessToken = accessToken,
|
|
|
|
|
ExpireTime = jwt.Payload.Exp.Value,
|
|
|
|
|
TokenType = "Bearer",
|
|
|
|
|
Scope = "api"
|
|
|
|
|
};
|
2024-02-05 16:00:28 +00:00
|
|
|
|
|
|
|
|
foreach (var hook in hooks)
|
|
|
|
|
{
|
|
|
|
|
hook.BeforeSending(token);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return token;
|
2023-06-11 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-07 22:04:34 +00:00
|
|
|
private string GenerateJwtToken(User user)
|
2023-06-11 23:46:02 +00:00
|
|
|
{
|
2024-01-31 19:57:54 +00:00
|
|
|
var claims = new List<Claim>
|
|
|
|
|
{
|
|
|
|
|
new Claim(JwtRegisteredClaimNames.NameId, user.Id),
|
|
|
|
|
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName),
|
|
|
|
|
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
2024-03-24 01:31:15 +00:00
|
|
|
new Claim(JwtRegisteredClaimNames.GivenName, user?.FirstName ?? string.Empty),
|
|
|
|
|
new Claim(JwtRegisteredClaimNames.FamilyName, user?.LastName ?? string.Empty),
|
2024-01-31 19:57:54 +00:00
|
|
|
new Claim("source", user.Source),
|
2024-03-24 01:31:15 +00:00
|
|
|
new Claim("external_id", user.ExternalId ?? string.Empty),
|
2024-01-31 19:57:54 +00:00
|
|
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var validators = _services.GetServices<IAuthenticationHook>();
|
|
|
|
|
foreach (var validator in validators)
|
|
|
|
|
{
|
|
|
|
|
validator.AddClaims(claims);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-11 23:46:02 +00:00
|
|
|
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
|
|
|
|
|
{
|
2024-01-31 19:57:54 +00:00
|
|
|
Subject = new ClaimsIdentity(claims),
|
|
|
|
|
Expires = DateTime.UtcNow.AddHours(2),
|
2023-06-11 23:46:02 +00:00
|
|
|
Issuer = issuer,
|
|
|
|
|
Audience = audience,
|
2024-01-31 19:57:54 +00:00
|
|
|
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
|
2024-02-14 23:45:28 +00:00
|
|
|
SecurityAlgorithms.HmacSha256Signature)
|
2023-06-11 23:46:02 +00:00
|
|
|
};
|
|
|
|
|
var tokenHandler = new JwtSecurityTokenHandler();
|
|
|
|
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
|
|
|
|
return tokenHandler.WriteToken(token);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-31 15:15:41 +00:00
|
|
|
[MemoryCache(10 * 60, perInstanceCache: true)]
|
2023-06-11 23:46:02 +00:00
|
|
|
public async Task<User> GetMyProfile()
|
|
|
|
|
{
|
2023-08-10 04:53:22 +00:00
|
|
|
var db = _services.GetRequiredService<IBotSharpRepository>();
|
2024-02-15 18:11:56 +00:00
|
|
|
User user = default;
|
2024-05-31 16:26:23 +00:00
|
|
|
|
|
|
|
|
if (_user.Id != null)
|
|
|
|
|
{
|
|
|
|
|
user = db.GetUserById(_user.Id);
|
|
|
|
|
}
|
|
|
|
|
else if (_user.UserName != null)
|
2024-02-15 18:11:56 +00:00
|
|
|
{
|
|
|
|
|
user = db.GetUserByUserName(_user.UserName);
|
|
|
|
|
}
|
|
|
|
|
else if (_user.Email != null)
|
|
|
|
|
{
|
|
|
|
|
user = db.GetUserByEmail(_user.Email);
|
|
|
|
|
}
|
2023-11-14 01:25:25 +00:00
|
|
|
return user;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 03:47:07 +00:00
|
|
|
[MemoryCache(10 * 60, perInstanceCache: true)]
|
2023-11-14 01:25:25 +00:00
|
|
|
public async Task<User> GetUser(string id)
|
|
|
|
|
{
|
|
|
|
|
var db = _services.GetRequiredService<IBotSharpRepository>();
|
|
|
|
|
var user = db.GetUserById(id);
|
2023-06-11 23:46:02 +00:00
|
|
|
return user;
|
|
|
|
|
}
|
2024-05-26 01:34:29 +00:00
|
|
|
|
|
|
|
|
public async Task<Token> ActiveUser(UserActivationModel model)
|
|
|
|
|
{
|
|
|
|
|
var id = model.UserName;
|
|
|
|
|
var db = _services.GetRequiredService<IBotSharpRepository>();
|
|
|
|
|
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
|
|
|
|
|
if (record == null)
|
|
|
|
|
{
|
|
|
|
|
record = db.GetUserByUserName(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (record == null)
|
|
|
|
|
{
|
|
|
|
|
return default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (record.VerificationCode != model.VerificationCode)
|
|
|
|
|
{
|
|
|
|
|
return default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (record.Verified)
|
|
|
|
|
{
|
|
|
|
|
return default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db.UpdateUserVerified(record.Id);
|
|
|
|
|
|
|
|
|
|
var accessToken = GenerateJwtToken(record);
|
|
|
|
|
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
|
|
|
|
|
var token = new Token
|
|
|
|
|
{
|
|
|
|
|
AccessToken = accessToken,
|
|
|
|
|
ExpireTime = jwt.Payload.Exp.Value,
|
|
|
|
|
TokenType = "Bearer",
|
|
|
|
|
Scope = "api"
|
|
|
|
|
};
|
|
|
|
|
return token;
|
|
|
|
|
}
|
2024-05-28 11:47:48 +00:00
|
|
|
|
2024-05-30 09:11:42 +00:00
|
|
|
public async Task<bool> VerifyUserNameExisting(string userName)
|
2024-05-28 11:47:48 +00:00
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(userName))
|
2024-05-30 09:11:42 +00:00
|
|
|
return true;
|
2024-05-28 11:47:48 +00:00
|
|
|
|
|
|
|
|
var db = _services.GetRequiredService<IBotSharpRepository>();
|
|
|
|
|
var user = db.GetUserByUserName(userName);
|
2024-05-30 09:11:42 +00:00
|
|
|
if (user != null)
|
2024-05-28 11:47:48 +00:00
|
|
|
return true;
|
2024-05-30 09:11:42 +00:00
|
|
|
|
2024-05-28 11:47:48 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-30 09:11:42 +00:00
|
|
|
public async Task<bool> VerifyEmailExisting(string email)
|
2024-05-28 11:47:48 +00:00
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(email))
|
2024-05-30 09:11:42 +00:00
|
|
|
return true;
|
2024-05-28 11:47:48 +00:00
|
|
|
|
|
|
|
|
var db = _services.GetRequiredService<IBotSharpRepository>();
|
|
|
|
|
var emailName = db.GetUserByEmail(email);
|
2024-05-30 09:11:42 +00:00
|
|
|
if (emailName != null)
|
2024-05-28 11:47:48 +00:00
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2023-06-11 23:46:02 +00:00
|
|
|
}
|