using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Infrastructures; using BotSharp.Abstraction.Users.Models; using BotSharp.Abstraction.Users.Settings; using BotSharp.OpenAPI.ViewModels.Users; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using NanoidDotNet; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text.RegularExpressions; using System.Net; namespace BotSharp.Core.Users.Services; public class UserService : IUserService { private readonly IServiceProvider _services; private readonly IUserIdentity _user; private readonly ILogger _logger; private readonly AccountSetting _setting; public UserService(IServiceProvider services, IUserIdentity user, ILogger logger, AccountSetting setting) { _services = services; _user = user; _logger = logger; _setting = setting; } public async Task CreateUser(User user) { if (string.IsNullOrEmpty(user.UserName)) { // generate unique name var name = Nanoid.Generate("0123456789botsharp", 10); user.UserName = name; } else { user.UserName = user.UserName.ToLower(); } var db = _services.GetRequiredService(); 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.IsNullOrWhiteSpace(user.Phone)) { record.Phone = "+" + Regex.Match(user.Phone, @"\d+").Value; } record.Salt = Guid.NewGuid().ToString("N"); record.Password = Utilities.HashTextMd5($"{user.Password}{record.Salt}"); if (_setting.NewUserVerification) { record.VerificationCode = Nanoid.Generate(alphabet: "0123456789", size: 6); record.Verified = false; } db.CreateUser(record); _logger.LogWarning($"Created new user account: {record.Id} {record.UserName}"); Utilities.ClearCache(); var hooks = _services.GetServices(); foreach (var hook in hooks) { await hook.UserCreated(record); } return record; } public async Task UpdatePassword(string password, string verificationCode) { var db = _services.GetRequiredService(); var record = db.GetUserByUserName(_user.UserName); if (record == null) { return false; } if (record.VerificationCode != verificationCode) { return false; } var newPassword = Utilities.HashTextMd5($"{password}{record.Salt}"); db.UpdateUserPassword(record.Id, newPassword); return true; } public async Task GetAffiliateToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); var (id, password) = base64.SplitAsTuple(":"); var db = _services.GetRequiredService(); var record = db.GetAffiliateUserByPhone(id); if (record == null) { record = db.GetUserByPhone(id); } var isCanLoginAffiliateRoleType = record != null && !record.IsDisabled && record.Type != UserType.Client; if (!isCanLoginAffiliateRoleType) { return default; } if (Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) { return default; } 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; } public async Task GetToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); var (id, password) = base64.SplitAsTuple(":"); var db = _services.GetRequiredService(); var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); if (record == null) { record = db.GetUserByUserName(id); } if (record != null && record.Type == UserType.Affiliate) { return default; } var hooks = _services.GetServices(); //verify password is correct or not. if (record != null && !hooks.Any()) { var hashPassword = Utilities.HashTextMd5($"{password}{record.Salt}"); if (hashPassword != record.Password) { return default; } } User? user = record; var isAuthenticatedByHook = false; if (record == null || record.Source != UserSource.Internal) { // check 3rd party user foreach (var hook in hooks) { user = await hook.Authenticate(id, password); if (user == null) { continue; } if (string.IsNullOrEmpty(user.Source) || user.Source == UserSource.Internal) { _logger.LogError($"Please set source name in the Authenticate hook."); return null; } if (record == null) { // create a local user record record = new User { UserName = user.UserName, Email = user.Email, FirstName = user.FirstName, LastName = user.LastName, Source = user.Source, ExternalId = user.ExternalId, Password = user.Password, Type = user.Type, Role = user.Role }; await CreateUser(record); } isAuthenticatedByHook = true; break; } } if ((hooks.Any() && user == null) || record == null) { return default; } if (!isAuthenticatedByHook && _setting.NewUserVerification && !record.Verified) { return default; } if (!isAuthenticatedByHook && Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) { return default; } 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" }; foreach (var hook in hooks) { hook.BeforeSending(token); } return token; } private string GenerateJwtToken(User user) { var claims = new List { new Claim(JwtRegisteredClaimNames.NameId, user.Id), new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName), new Claim(JwtRegisteredClaimNames.Email, user?.Email ?? string.Empty), new Claim(JwtRegisteredClaimNames.GivenName, user?.FirstName ?? string.Empty), new Claim(JwtRegisteredClaimNames.FamilyName, user?.LastName ?? string.Empty), new Claim("source", user.Source), new Claim("external_id", user.ExternalId ?? string.Empty), new Claim("type", user.Type ?? UserType.Client), new Claim("role", user.Role ?? UserRole.User), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim("phone", user.Phone ?? string.Empty), new Claim("affiliateId", user.AffiliateId ?? string.Empty) }; var validators = _services.GetServices(); foreach (var validator in validators) { validator.AddClaims(claims); } var config = _services.GetRequiredService(); var issuer = config["Jwt:Issuer"]; var audience = config["Jwt:Audience"]; var expireInMinutes = int.Parse(config["Jwt:ExpireInMinutes"] ?? "120"); var key = Encoding.ASCII.GetBytes(config["Jwt:Key"]); var expires = DateTime.UtcNow.AddMinutes(expireInMinutes); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = expires, Issuer = issuer, Audience = audience, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); SaveUserTokenExpiresCache(user.Id, expires, expireInMinutes).GetAwaiter().GetResult(); return tokenHandler.WriteToken(token); } private async Task SaveUserTokenExpiresCache(string userId, DateTime expires, int expireInMinutes) { var _cacheService = _services.GetRequiredService(); await _cacheService.SetAsync(GetUserTokenExpiresCacheKey(userId), expires, TimeSpan.FromMinutes(expireInMinutes)); } private string GetUserTokenExpiresCacheKey(string userId) { return $"user_{userId}_token_expires"; } public async Task GetUserTokenExpires() { var _cacheService = _services.GetRequiredService(); return await _cacheService.GetAsync(GetUserTokenExpiresCacheKey(_user.Id)); } [MemoryCache(10 * 60, perInstanceCache: true)] public async Task GetMyProfile() { var db = _services.GetRequiredService(); User user = default; if (_user.Id != null) { user = db.GetUserById(_user.Id); } else if (_user.UserName != null) { user = db.GetUserByUserName(_user.UserName); } else if (_user.Email != null) { user = db.GetUserByEmail(_user.Email); } return user; } [MemoryCache(10 * 60, perInstanceCache: true)] public async Task GetUser(string id) { var db = _services.GetRequiredService(); var user = db.GetUserById(id); return user; } public async Task ActiveUser(UserActivationModel model) { var id = model.UserName; var db = _services.GetRequiredService(); 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; } public async Task VerifyUserNameExisting(string userName) { if (string.IsNullOrEmpty(userName)) { return true; } var db = _services.GetRequiredService(); var user = db.GetUserByUserName(userName); if (user != null) { return true; } return false; } public async Task VerifyEmailExisting(string email) { if (string.IsNullOrEmpty(email)) { return true; } var db = _services.GetRequiredService(); var emailName = db.GetUserByEmail(email); if (emailName != null) { return true; } return false; } public async Task SendVerificationCodeResetPassword(User user) { var db = _services.GetRequiredService(); User? record = null; if (!string.IsNullOrWhiteSpace(_user.Id)) { record = db.GetUserById(_user.Id); } else { if (!string.IsNullOrEmpty(user.Email) && !string.IsNullOrEmpty(user.Phone)) { return false; } if (!string.IsNullOrEmpty(user.Email)) { record = db.GetUserByEmail(user.Email); } if (!string.IsNullOrEmpty(user.Phone)) { record = db.GetUserByPhone(user.Phone); } } if (record == null) { return false; } record.VerificationCode = Nanoid.Generate(alphabet: "0123456789", size: 6); //update current verification code. db.UpdateUserVerificationCode(record.Id, record.VerificationCode); //send code to user Email. var hooks = _services.GetServices(); foreach (var hook in hooks) { hook.VerificationCodeResetPassword(record); } return true; } public async Task ResetUserPassword(User user) { if (!string.IsNullOrEmpty(user.Email) && !string.IsNullOrEmpty(user.Phone)) { return false; } var db = _services.GetRequiredService(); User? record = null; if (!string.IsNullOrEmpty(user.Email)) { record = db.GetUserByEmail(user.Email); } if (!string.IsNullOrEmpty(user.Phone)) { record = db.GetUserByPhone(user.Phone); } if (record == null) { return false; } if (user.VerificationCode != record.VerificationCode) { return false; } var newPassword = Utilities.HashTextMd5($"{user.Password}{record.Salt}"); db.UpdateUserPassword(record.Id, newPassword); return true; } public async Task ModifyUserEmail(string email) { var curUser = await GetMyProfile(); var db = _services.GetRequiredService(); var record = db.GetUserById(curUser.Id); if (record == null) { return false; } db.UpdateUserEmail(record.Id, email); return true; } public async Task ModifyUserPhone(string phone) { var curUser = await GetMyProfile(); var db = _services.GetRequiredService(); var record = db.GetUserById(curUser.Id); if (record == null) { return false; } db.UpdateUserPhone(record.Id, phone); return true; } }