commit
fd0897019b
|
|
@ -32,6 +32,7 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
|
||||
<PackageReference Include="System.Memory.Data" Version="8.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ public class BrowserActionResult
|
|||
public bool IsSuccess { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? StackTrace { get; set; }
|
||||
public string Selector { get; set; }
|
||||
public string Body { get; set; }
|
||||
public string? Selector { get; set; }
|
||||
public string? Body { get; set; }
|
||||
public bool IsHighlighted { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using BotSharp.Abstraction.Browsing.Enums;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class ElementActionArgs
|
||||
{
|
||||
public BroswerActionEnum Action { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class ElementLocatingArgs
|
||||
{
|
||||
[JsonPropertyName("match_rule")]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using BotSharp.Abstraction.Infrastructures;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class MessageInfo : ICacheKey
|
||||
{
|
||||
public string AgentId { get; set; } = null!;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
using BotSharp.Abstraction.Browsing.Enums;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class PageActionArgs
|
||||
{
|
||||
public BroswerActionEnum Action { get; set; }
|
||||
|
||||
public string? Content { get; set; }
|
||||
public string? Direction { get; set; }
|
||||
public string Direction { get; set; } = "down";
|
||||
|
||||
public string Url { get; set; } = null!;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,9 @@ public class WebPageResponseData
|
|||
public string ResponseData { get; set; } = null!;
|
||||
public bool ResponseInMemory { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Url} {ResponseData.Length}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class WebPageResponseFilter
|
||||
{
|
||||
public string Url { get; set; } = null!;
|
||||
public string[]? QueryParameters { get; set; }
|
||||
|
||||
public string[]? PostData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// contains, starts, ends, equals
|
||||
/// </summary>
|
||||
public string UrlMatchPattern { get; set; } = "contains";
|
||||
|
||||
/// <summary>
|
||||
/// Handle Content-Type: text/x-component
|
||||
/// </summary>
|
||||
public Func<string, string>? PartSearch { get; set; } = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ public class SharpCacheAttribute : MoAttribute
|
|||
|
||||
private string GetCacheKey(SharpCacheSettings settings, MethodContext context)
|
||||
{
|
||||
var key = settings.Prefix + "-" + context.Method.Name;
|
||||
var key = settings.Prefix + ":" + context.Method.Name;
|
||||
foreach (var arg in context.Arguments)
|
||||
{
|
||||
if (arg is null)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public interface IBotSharpRepository
|
|||
User? GetAffiliateUserByPhone(string phone) => throw new NotImplementedException();
|
||||
User? GetUserById(string id) => throw new NotImplementedException();
|
||||
List<User> GetUserByIds(List<string> ids) => throw new NotImplementedException();
|
||||
User? GetUserByAffiliateId(string affiliateId) => throw new NotImplementedException();
|
||||
List<User> GetUsersByAffiliateId(string affiliateId) => throw new NotImplementedException();
|
||||
User? GetUserByUserName(string userName) => throw new NotImplementedException();
|
||||
void CreateUser(User user) => throw new NotImplementedException();
|
||||
void UpdateExistUser(string userId, User user) => throw new NotImplementedException();
|
||||
|
|
@ -32,7 +32,7 @@ public interface IBotSharpRepository
|
|||
void UpdateUserVerificationCode(string userId, string verficationCode) => throw new NotImplementedException();
|
||||
void UpdateUserPassword(string userId, string password) => throw new NotImplementedException();
|
||||
void UpdateUserEmail(string userId, string email) => throw new NotImplementedException();
|
||||
void UpdateUserPhone(string userId, string Iphone) => throw new NotImplementedException();
|
||||
void UpdateUserPhone(string userId, string Iphone, string regionCode) => throw new NotImplementedException();
|
||||
void UpdateUserIsDisable(string userId, bool isDisable) => throw new NotImplementedException();
|
||||
void UpdateUsersIsDisable(List<string> userIds, bool isDisable) => throw new NotImplementedException();
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -1,15 +1,60 @@
|
|||
using BotSharp.Abstraction.Users.Models;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BotSharp.Abstraction.Users;
|
||||
|
||||
public interface IAuthenticationHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Interupt the authentication process, and return the user object if the user is authenticated
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
Task<User> Authenticate(string id, string password);
|
||||
void AddClaims(List<Claim> claims);
|
||||
void BeforeSending(Token token);
|
||||
|
||||
/// <summary>
|
||||
/// Add extra claims to user
|
||||
/// </summary>
|
||||
/// <param name="claims"></param>
|
||||
/// <returns></returns>
|
||||
bool AddClaims(List<Claim> claims)
|
||||
=> true;
|
||||
|
||||
/// <summary>
|
||||
/// User authenticated successfully
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
bool UserAuthenticated(JwtSecurityToken token)
|
||||
=> true;
|
||||
|
||||
/// <summary>
|
||||
/// Bfore user updating
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
Task UserUpdating(User user);
|
||||
|
||||
/// <summary>
|
||||
/// After user created
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
Task UserCreated(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Reset password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
Task VerificationCodeResetPassword(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Delete users
|
||||
/// </summary>
|
||||
/// <param name="userIds"></param>
|
||||
/// <returns></returns>
|
||||
Task DelUsers(List<string> userIds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,8 @@ public interface IUserIdentity
|
|||
string UserLanguage { get; }
|
||||
string? Phone { get; }
|
||||
string? AffiliateId { get; }
|
||||
string? EmployeeId { get; }
|
||||
string Type { get; }
|
||||
string Role { get; }
|
||||
string? RegionCode { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ public interface IUserService
|
|||
Task<User> CreateUser(User user);
|
||||
Task<Token> ActiveUser(UserActivationModel model);
|
||||
Task<Token?> GetAffiliateToken(string authorization);
|
||||
Task<Token?> GetAdminToken(string authorization);
|
||||
Task<Token?> GetToken(string authorization);
|
||||
Task<User> GetMyProfile();
|
||||
Task<bool> VerifyUserNameExisting(string userName);
|
||||
|
|
@ -18,7 +19,7 @@ public interface IUserService
|
|||
Task<bool> SendVerificationCodeResetPasswordLogin();
|
||||
Task<bool> ResetUserPassword(User user);
|
||||
Task<bool> ModifyUserEmail(string email);
|
||||
Task<bool> ModifyUserPhone(string phone);
|
||||
Task<bool> ModifyUserPhone(string phone, string regionCode);
|
||||
Task<bool> UpdatePassword(string newPassword, string verificationCode);
|
||||
Task<DateTime> GetUserTokenExpires();
|
||||
Task<bool> UpdateUsersIsDisable(List<string> userIds, bool isDisable);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ public class User
|
|||
public string Role { get; set; } = UserRole.User;
|
||||
public string? VerificationCode { get; set; }
|
||||
public bool Verified { get; set; }
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
public string? AffiliateId { get; set; }
|
||||
public string? EmployeeId { get; set; }
|
||||
public bool IsDisabled { get; set; }
|
||||
public DateTime UpdatedTime { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ public class AccountSetting
|
|||
/// </summary>
|
||||
public bool NewUserVerification { get; set; }
|
||||
public string[] AllowMultipleDeviceLoginUserIds { get; set; } = [];
|
||||
public bool CreateUserAutomatically { get; set; } = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ public partial class FileRepository
|
|||
return Users.Where(x => ids.Contains(x.Id) || (x.ExternalId != null && ids.Contains(x.ExternalId)))?.ToList() ?? new List<User>();
|
||||
}
|
||||
|
||||
public User? GetUserByAffiliateId(string affiliateId)
|
||||
public List<User> GetUsersByAffiliateId(string affiliateId)
|
||||
{
|
||||
return Users.FirstOrDefault(x => x.AffiliateId == affiliateId);
|
||||
return Users.Where(x => x.AffiliateId == affiliateId).ToList();
|
||||
}
|
||||
|
||||
public User? GetUserByUserName(string userName = null)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class UserIdentity : IUserIdentity
|
|||
get
|
||||
{
|
||||
_contextAccessor.HttpContext.Request.Headers.TryGetValue("User-Language", out var languages);
|
||||
return languages.FirstOrDefault() ?? "en-US";
|
||||
return languages.FirstOrDefault() ?? "en-US";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,4 +72,16 @@ public class UserIdentity : IUserIdentity
|
|||
|
||||
[JsonPropertyName("affiliateId")]
|
||||
public string? AffiliateId => _claims?.FirstOrDefault(x => x.Type == "affiliateId")?.Value;
|
||||
|
||||
[JsonPropertyName("employeeId")]
|
||||
public string? EmployeeId => _claims?.FirstOrDefault(x => x.Type == "employeeId")?.Value;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type => _claims?.FirstOrDefault(x => x.Type == "type")?.Value;
|
||||
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Role)?.Value;
|
||||
|
||||
[JsonPropertyName("regionCode")]
|
||||
public string? RegionCode => _claims?.FirstOrDefault(x => x.Type == "regionCode")?.Value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ public class UserService : IUserService
|
|||
record.Email = user.Email?.ToLower();
|
||||
if (!string.IsNullOrWhiteSpace(user.Phone))
|
||||
{
|
||||
record.Phone = "+" + Regex.Match(user.Phone, @"\d+").Value;
|
||||
//record.Phone = "+" + Regex.Match(user.Phone, @"\d+").Value;
|
||||
record.Phone = Regex.Match(user.Phone, @"\d+").Value;
|
||||
}
|
||||
record.Salt = Guid.NewGuid().ToString("N");
|
||||
record.Password = Utilities.HashTextMd5($"{user.Password}{record.Salt}");
|
||||
|
|
@ -144,19 +145,14 @@ public class UserService : IUserService
|
|||
return true;
|
||||
}
|
||||
|
||||
public async Task<Token> GetAffiliateToken(string authorization)
|
||||
public async Task<Token?> GetAffiliateToken(string authorization)
|
||||
{
|
||||
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
|
||||
var (id, password) = base64.SplitAsTuple(":");
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetAffiliateUserByPhone(id);
|
||||
if (record == null)
|
||||
{
|
||||
record = db.GetUserByPhone(id);
|
||||
}
|
||||
|
||||
var isCanLoginAffiliateRoleType = record != null && !record.IsDisabled && record.Type != UserType.Client;
|
||||
if (!isCanLoginAffiliateRoleType)
|
||||
var isCanLogin = record != null && !record.IsDisabled && record.Type == UserType.Affiliate;
|
||||
if (!isCanLogin)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
|
@ -166,6 +162,39 @@ public class UserService : IUserService
|
|||
return default;
|
||||
}
|
||||
|
||||
var (token, jwt) = BuildToken(record);
|
||||
|
||||
return await Task.FromResult(token);
|
||||
}
|
||||
|
||||
public async Task<Token?> GetAdminToken(string authorization)
|
||||
{
|
||||
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
|
||||
var (id, password) = base64.SplitAsTuple(":");
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetUserByPhone(id);
|
||||
var isCanLogin = record != null && !record.IsDisabled
|
||||
&& record.Type == UserType.Internal && new List<string>
|
||||
{
|
||||
UserRole.Root,UserRole.Admin
|
||||
}.Contains(record.Role);
|
||||
if (!isCanLogin)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var (token, jwt) = BuildToken(record);
|
||||
|
||||
return await Task.FromResult(token);
|
||||
}
|
||||
|
||||
private (Token, JwtSecurityToken) BuildToken(User record)
|
||||
{
|
||||
var accessToken = GenerateJwtToken(record);
|
||||
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
|
||||
var token = new Token
|
||||
|
|
@ -175,7 +204,7 @@ public class UserService : IUserService
|
|||
TokenType = "Bearer",
|
||||
Scope = "api"
|
||||
};
|
||||
return token;
|
||||
return (token, jwt);
|
||||
}
|
||||
|
||||
public async Task<Token?> GetToken(string authorization)
|
||||
|
|
@ -187,7 +216,7 @@ public class UserService : IUserService
|
|||
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
|
||||
if (record == null)
|
||||
{
|
||||
record = db.GetUserByUserName(id);
|
||||
record = db.GetUserByPhone(id);
|
||||
}
|
||||
|
||||
if (record != null && record.Type == UserType.Affiliate)
|
||||
|
|
@ -238,7 +267,8 @@ public class UserService : IUserService
|
|||
ExternalId = user.ExternalId,
|
||||
Password = user.Password,
|
||||
Type = user.Type,
|
||||
Role = user.Role
|
||||
Role = user.Role,
|
||||
RegionCode = user.RegionCode
|
||||
};
|
||||
await CreateUser(record);
|
||||
}
|
||||
|
|
@ -263,19 +293,10 @@ public class UserService : IUserService
|
|||
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"
|
||||
};
|
||||
|
||||
var (token, jwt) = BuildToken(record);
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
hook.BeforeSending(token);
|
||||
hook.UserAuthenticated(jwt);
|
||||
}
|
||||
|
||||
return token;
|
||||
|
|
@ -296,7 +317,9 @@ public class UserService : IUserService
|
|||
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)
|
||||
new Claim("affiliateId", user.AffiliateId ?? string.Empty),
|
||||
new Claim("employeeId", user.EmployeeId ?? string.Empty),
|
||||
new Claim("regionCode", user.RegionCode ?? "CN")
|
||||
};
|
||||
|
||||
var validators = _services.GetServices<IAuthenticationHook>();
|
||||
|
|
@ -339,7 +362,7 @@ public class UserService : IUserService
|
|||
|
||||
private string GetUserTokenExpiresCacheKey(string userId)
|
||||
{
|
||||
return $"user_{userId}_token_expires";
|
||||
return $"user:{userId}_token_expires";
|
||||
}
|
||||
|
||||
public async Task<DateTime> GetUserTokenExpires()
|
||||
|
|
@ -384,7 +407,7 @@ public class UserService : IUserService
|
|||
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
|
||||
if (record == null)
|
||||
{
|
||||
record = db.GetUserByUserName(id);
|
||||
record = db.GetUserByPhone(id);
|
||||
}
|
||||
|
||||
if (record == null)
|
||||
|
|
@ -597,19 +620,26 @@ public class UserService : IUserService
|
|||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ModifyUserPhone(string phone)
|
||||
public async Task<bool> ModifyUserPhone(string phone, string regionCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(regionCode))
|
||||
{
|
||||
throw new Exception("regionCode is required");
|
||||
}
|
||||
var curUser = await GetMyProfile();
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetUserById(curUser.Id);
|
||||
var existPhone = db.GetUserByPhone(phone);
|
||||
|
||||
if (record == null || existPhone != null)
|
||||
if (record == null || (existPhone != null && existPhone.RegionCode == regionCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
record.Phone = phone;
|
||||
record.RegionCode = regionCode;
|
||||
record.UserName = phone;
|
||||
record.FirstName = phone;
|
||||
|
||||
var hooks = _services.GetServices<IAuthenticationHook>();
|
||||
foreach (var hook in hooks)
|
||||
|
|
@ -617,7 +647,7 @@ public class UserService : IUserService
|
|||
await hook.UserUpdating(record);
|
||||
}
|
||||
|
||||
db.UpdateUserPhone(record.Id, record.Phone);
|
||||
db.UpdateUserPhone(record.Id, record.Phone, regionCode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Users.Settings;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
|
@ -10,10 +11,12 @@ public class UserController : ControllerBase
|
|||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IUserService _userService;
|
||||
public UserController(IUserService userService, IServiceProvider services)
|
||||
private readonly AccountSetting _setting;
|
||||
public UserController(IUserService userService, IServiceProvider services, AccountSetting setting)
|
||||
{
|
||||
_services = services;
|
||||
_userService = userService;
|
||||
_setting = setting;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
|
@ -77,7 +80,7 @@ public class UserController : ControllerBase
|
|||
public async Task<UserViewModel> GetMyUserProfile()
|
||||
{
|
||||
var user = await _userService.GetMyProfile();
|
||||
if (user == null)
|
||||
if (user == null && _setting.CreateUserAutomatically)
|
||||
{
|
||||
var identiy = _services.GetRequiredService<IUserIdentity>();
|
||||
var accessor = _services.GetRequiredService<IHttpContextAccessor>();
|
||||
|
|
@ -90,6 +93,8 @@ public class UserController : ControllerBase
|
|||
LastName = identiy.LastName,
|
||||
Source = claims.First().Issuer,
|
||||
ExternalId = identiy.Id,
|
||||
RegionCode = identiy.RegionCode,
|
||||
Phone = identiy.Phone,
|
||||
});
|
||||
}
|
||||
return UserViewModel.FromUser(user);
|
||||
|
|
@ -153,9 +158,9 @@ public class UserController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost("/user/phone/modify")]
|
||||
public async Task<bool> ModifyUserPhone([FromQuery] string phone)
|
||||
public async Task<bool> ModifyUserPhone([FromQuery] string phone, [FromQuery] string regionCode = "CN")
|
||||
{
|
||||
return await _userService.ModifyUserPhone(phone);
|
||||
return await _userService.ModifyUserPhone(phone, regionCode);
|
||||
}
|
||||
|
||||
[HttpPost("/user/update/isdisable")]
|
||||
|
|
|
|||
|
|
@ -1,80 +1,78 @@
|
|||
using BotSharp.Abstraction.Users.Settings;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
namespace BotSharp.OpenAPI.Filters
|
||||
{
|
||||
public class UserSingleLoginFilter : IAuthorizationFilter
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IServiceProvider _services;
|
||||
namespace BotSharp.OpenAPI.Filters;
|
||||
|
||||
public UserSingleLoginFilter(IUserService userService, IServiceProvider services)
|
||||
public class UserSingleLoginFilter : IAuthorizationFilter
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public UserSingleLoginFilter(IUserService userService, IServiceProvider services)
|
||||
{
|
||||
_userService = userService;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
var isAllowAnonymous = context.ActionDescriptor.EndpointMetadata
|
||||
.Any(em => em.GetType() == typeof(AllowAnonymousAttribute));
|
||||
|
||||
if (isAllowAnonymous)
|
||||
{
|
||||
_userService = userService;
|
||||
_services = services;
|
||||
return;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
var bearerToken = GetBearerToken(context);
|
||||
if (!string.IsNullOrWhiteSpace(bearerToken))
|
||||
{
|
||||
var isAllowAnonymous = context.ActionDescriptor.EndpointMetadata
|
||||
.Any(em => em.GetType() == typeof(AllowAnonymousAttribute));
|
||||
var config = _services.GetRequiredService<AccountSetting>();
|
||||
var token = GetJwtToken(bearerToken);
|
||||
|
||||
if (isAllowAnonymous)
|
||||
if (config.AllowMultipleDeviceLoginUserIds.Contains(token.Claims.First(x => x.Type == "nameid").Value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bearerToken = GetBearerToken(context);
|
||||
if (!string.IsNullOrWhiteSpace(bearerToken))
|
||||
var validTo = token.ValidTo.ToLongTimeString();
|
||||
var currentExpires = GetUserExpires().ToLongTimeString();
|
||||
|
||||
if (validTo != currentExpires)
|
||||
{
|
||||
var config = _services.GetRequiredService<AccountSetting>();
|
||||
var token = GetJwtToken(bearerToken);
|
||||
|
||||
if (config.AllowMultipleDeviceLoginUserIds.Contains(token.Claims.First(x => x.Type == "nameid").Value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var validTo = token.ValidTo.ToLongTimeString();
|
||||
var currentExpires = GetUserExpires().ToLongTimeString();
|
||||
|
||||
if (validTo != currentExpires)
|
||||
{
|
||||
Serilog.Log.Warning($"Token expired. Token expires at {validTo}, current expires at {currentExpires}");
|
||||
// login confict
|
||||
context.Result = new ConflictResult();
|
||||
}
|
||||
Serilog.Log.Warning($"Token expired. Token expires at {validTo}, current expires at {currentExpires}");
|
||||
// login confict
|
||||
context.Result = new ConflictResult();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetBearerToken(AuthorizationFilterContext context)
|
||||
{
|
||||
if (context.HttpContext.Request.Headers.TryGetValue(HeaderNames.Authorization, out var bearerToken)
|
||||
&& !string.IsNullOrWhiteSpace(bearerToken.ToString()))
|
||||
{
|
||||
var tokenType = bearerToken.ToString().Split(" ").First();
|
||||
if (tokenType == JwtBearerDefaults.AuthenticationScheme)
|
||||
{
|
||||
return bearerToken.ToString().Split(" ").Last();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JwtSecurityToken GetJwtToken(string jwtToken)
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(jwtToken);
|
||||
return token;
|
||||
}
|
||||
|
||||
private DateTime GetUserExpires()
|
||||
{
|
||||
return _userService.GetUserTokenExpires().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetBearerToken(AuthorizationFilterContext context)
|
||||
{
|
||||
if (context.HttpContext.Request.Headers.TryGetValue(HeaderNames.Authorization, out var bearerToken)
|
||||
&& !string.IsNullOrWhiteSpace(bearerToken.ToString()))
|
||||
{
|
||||
var tokenType = bearerToken.ToString().Split(" ").First();
|
||||
if (tokenType == JwtBearerDefaults.AuthenticationScheme)
|
||||
{
|
||||
return bearerToken.ToString().Split(" ").Last();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JwtSecurityToken GetJwtToken(string jwtToken)
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(jwtToken);
|
||||
return token;
|
||||
}
|
||||
|
||||
private DateTime GetUserExpires()
|
||||
{
|
||||
return _userService.GetUserTokenExpires().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,18 @@ namespace BotSharp.OpenAPI.ViewModels.Users;
|
|||
public class UserCreationModel
|
||||
{
|
||||
public string? UserName { get; set; }
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string? LastName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = UserType.Client;
|
||||
public string Role { get; set; } = UserRole.User;
|
||||
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
public User ToUser()
|
||||
{
|
||||
return new User
|
||||
{
|
||||
return new User
|
||||
{
|
||||
UserName = UserName,
|
||||
FirstName = FirstName,
|
||||
LastName = LastName,
|
||||
|
|
@ -24,7 +24,8 @@ public class UserCreationModel
|
|||
Phone = Phone,
|
||||
Password = Password,
|
||||
Role = Role,
|
||||
Type = Type
|
||||
Type = Type,
|
||||
RegionCode = RegionCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Data;
|
||||
|
||||
namespace BotSharp.OpenAPI.ViewModels.Users;
|
||||
|
||||
public class UserResetPasswordModel
|
||||
|
|
@ -8,6 +6,7 @@ public class UserResetPasswordModel
|
|||
public string? Phone { get; set; }
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string VerificationCode { get; set; }
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
|
||||
public User ToUser()
|
||||
{
|
||||
|
|
@ -16,7 +15,8 @@ public class UserResetPasswordModel
|
|||
Email = Email,
|
||||
Phone = Phone,
|
||||
Password = Password,
|
||||
VerificationCode = VerificationCode
|
||||
VerificationCode = VerificationCode,
|
||||
RegionCode = RegionCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public class UserViewModel
|
|||
[JsonPropertyName("update_date")]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
|
||||
public static UserViewModel FromUser(User user)
|
||||
{
|
||||
if (user == null)
|
||||
|
|
@ -47,14 +49,15 @@ public class UserViewModel
|
|||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Email = user.Email,
|
||||
Phone = user.Phone,
|
||||
Phone = user.Phone?.Substring(0, 3) == "+86" ? user.Phone.Substring(3) : user.Phone,
|
||||
Type = user.Type,
|
||||
Role = user.Role,
|
||||
Source = user.Source,
|
||||
ExternalId = user.ExternalId,
|
||||
CreateDate = user.CreatedTime,
|
||||
UpdateDate = user.UpdatedTime,
|
||||
Avatar = "/user/avatar"
|
||||
Avatar = "/user/avatar",
|
||||
RegionCode = string.IsNullOrWhiteSpace(user.RegionCode) ? "CN" : user.RegionCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.MongoDB.Driver" Version="8.0.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.27.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.28.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ public class UserDocument : MongoBase
|
|||
public string Role { get; set; } = null!;
|
||||
public string? VerificationCode { get; set; }
|
||||
public bool Verified { get; set; }
|
||||
public string? RegionCode { get; set; }
|
||||
public string? AffiliateId { get; set; }
|
||||
public string? EmployeeId { get; set; }
|
||||
public bool IsDisabled { get; set; }
|
||||
public DateTime CreatedTime { get; set; }
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
|
|
@ -40,9 +42,11 @@ public class UserDocument : MongoBase
|
|||
Type = Type,
|
||||
Role = Role,
|
||||
AffiliateId = AffiliateId,
|
||||
EmployeeId = EmployeeId,
|
||||
IsDisabled = IsDisabled,
|
||||
VerificationCode = VerificationCode,
|
||||
Verified = Verified,
|
||||
RegionCode = RegionCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,21 @@ public partial class MongoRepository
|
|||
|
||||
public User? GetUserByPhone(string phone)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Phone == phone && x.Type != UserType.Affiliate);
|
||||
string phoneSecond = string.Empty;
|
||||
// 如果电话号码长度小于 4,直接返回 null
|
||||
if (phone?.Length < 4)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (phone.Substring(0, 3) != "+86")
|
||||
{
|
||||
phoneSecond = $"+86{phone}";
|
||||
}
|
||||
else
|
||||
{
|
||||
phoneSecond = phone.Replace("+86", "");
|
||||
}
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => (x.Phone == phone || x.Phone == phoneSecond) && x.Type != UserType.Affiliate);
|
||||
return user != null ? user.ToUser() : null;
|
||||
}
|
||||
|
||||
|
|
@ -37,11 +51,11 @@ public partial class MongoRepository
|
|||
return users?.Any() == true ? users.Select(x => x.ToUser()).ToList() : new List<User>();
|
||||
}
|
||||
|
||||
public User? GetUserByAffiliateId(string affiliateId)
|
||||
public List<User> GetUsersByAffiliateId(string affiliateId)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable()
|
||||
.FirstOrDefault(x => x.AffiliateId == affiliateId);
|
||||
return user != null ? user.ToUser() : null;
|
||||
var users = _dc.Users.AsQueryable()
|
||||
.Where(x => x.AffiliateId == affiliateId).ToList();
|
||||
return users?.Any() == true ? users.Select(x => x.ToUser()).ToList() : new List<User>();
|
||||
}
|
||||
|
||||
public User? GetUserByUserName(string userName)
|
||||
|
|
@ -70,7 +84,9 @@ public partial class MongoRepository
|
|||
Type = user.Type,
|
||||
VerificationCode = user.VerificationCode,
|
||||
Verified = user.Verified,
|
||||
RegionCode = user.RegionCode,
|
||||
AffiliateId = user.AffiliateId,
|
||||
EmployeeId = user.EmployeeId,
|
||||
IsDisabled = user.IsDisabled,
|
||||
CreatedTime = DateTime.UtcNow,
|
||||
UpdatedTime = DateTime.UtcNow
|
||||
|
|
@ -87,7 +103,9 @@ public partial class MongoRepository
|
|||
.Set(x => x.Phone, user.Phone)
|
||||
.Set(x => x.Salt, user.Salt)
|
||||
.Set(x => x.Password, user.Password)
|
||||
.Set(x => x.VerificationCode, user.VerificationCode);
|
||||
.Set(x => x.VerificationCode, user.VerificationCode)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Set(x => x.RegionCode, user.RegionCode);
|
||||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +129,8 @@ public partial class MongoRepository
|
|||
{
|
||||
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
|
||||
var update = Builders<UserDocument>.Update.Set(x => x.Password, password)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Set(x => x.Verified, true);
|
||||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +142,14 @@ public partial class MongoRepository
|
|||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
public void UpdateUserPhone(string userId, string phone)
|
||||
public void UpdateUserPhone(string userId, string phone, string regionCode)
|
||||
{
|
||||
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
|
||||
var update = Builders<UserDocument>.Update.Set(x => x.Phone, phone)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Set(x => x.RegionCode, regionCode)
|
||||
.Set(x => x.UserName, phone)
|
||||
.Set(x => x.FirstName, phone);
|
||||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public class PlaywrightInstance : IDisposable
|
|||
public IServiceProvider Services => _services;
|
||||
Dictionary<string, IBrowserContext> _contexts = new Dictionary<string, IBrowserContext>();
|
||||
Dictionary<string, List<IPage>> _pages = new Dictionary<string, List<IPage>>();
|
||||
IPage? _activePage = null;
|
||||
Dictionary<string, IPage?> _activePage = new Dictionary<string, IPage?>();
|
||||
|
||||
/// <summary>
|
||||
/// ContextId and BrowserContext
|
||||
|
|
@ -30,14 +30,14 @@ public class PlaywrightInstance : IDisposable
|
|||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
{
|
||||
return _activePage ?? _contexts[contextId].Pages.LastOrDefault();
|
||||
return _activePage.ContainsKey(contextId) ? _activePage[contextId] : _contexts[contextId].Pages.LastOrDefault();
|
||||
}
|
||||
|
||||
foreach (var page in _contexts[contextId].Pages)
|
||||
{
|
||||
if (page.Url.ToLower() == pattern.ToLower())
|
||||
{
|
||||
_activePage = page;
|
||||
_activePage[contextId] = page;
|
||||
page.BringToFrontAsync().Wait();
|
||||
return page;
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ public class PlaywrightInstance : IDisposable
|
|||
|
||||
_contexts[ctxId].Page += async (sender, page) =>
|
||||
{
|
||||
_activePage = page;
|
||||
_activePage[ctxId] = page;
|
||||
_pages[ctxId].Add(page);
|
||||
page.Close += async (sender, e) =>
|
||||
{
|
||||
|
|
@ -146,37 +146,41 @@ public class PlaywrightInstance : IDisposable
|
|||
{
|
||||
if (e.Status != 204 &&
|
||||
e.Headers.ContainsKey("content-type") &&
|
||||
e.Headers["content-type"].Contains("application/json") &&
|
||||
(e.Request.ResourceType == "fetch" || e.Request.ResourceType == "xhr") &&
|
||||
(excludeResponseUrls == null || !excludeResponseUrls.Any(url => e.Url.ToLower().Contains(url))) &&
|
||||
(includeResponseUrls == null || includeResponseUrls.Any(url => e.Url.ToLower().Contains(url))))
|
||||
{
|
||||
Serilog.Log.Information($"{e.Request.Method}: {e.Url}");
|
||||
JsonElement? json = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (e.Status == 200 && e.Ok)
|
||||
{
|
||||
json = await e.JsonAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Warning($"Response status: {e.Status} {e.StatusText}, OK: {e.Ok}");
|
||||
}
|
||||
|
||||
var result = new WebPageResponseData
|
||||
{
|
||||
Url = e.Url.ToLower(),
|
||||
PostData = e.Request?.PostData ?? string.Empty,
|
||||
ResponseData = JsonSerializer.Serialize(json),
|
||||
ResponseInMemory = responseInMemory
|
||||
};
|
||||
|
||||
if (e.Headers["content-type"].Contains("application/json"))
|
||||
{
|
||||
if (e.Status == 200 && e.Ok)
|
||||
{
|
||||
var json = await e.JsonAsync();
|
||||
result.ResponseData = JsonSerializer.Serialize(json);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var html = await e.TextAsync();
|
||||
result.ResponseData = html;
|
||||
}
|
||||
|
||||
if (responseContainer != null && responseInMemory)
|
||||
{
|
||||
responseContainer.Add(result);
|
||||
}
|
||||
|
||||
Serilog.Log.Warning($"Response status: {e.Status} {e.StatusText}, OK: {e.Ok}");
|
||||
var webPageResponseHooks = _services.GetServices<IWebPageResponseHook>();
|
||||
foreach (var hook in webPageResponseHooks)
|
||||
{
|
||||
|
|
@ -236,7 +240,7 @@ public class PlaywrightInstance : IDisposable
|
|||
if (page != null)
|
||||
{
|
||||
await page.CloseAsync();
|
||||
_activePage = _pages[ctxId].LastOrDefault();
|
||||
_activePage[ctxId] = _pages[ctxId].LastOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,13 +122,21 @@ public partial class PlaywrightWebDriver
|
|||
|
||||
foreach (var element in await locator.AllAsync())
|
||||
{
|
||||
var content = await element.InnerHTMLAsync();
|
||||
var content = await element.EvaluateAsync<string>("element => element.outerHTML");
|
||||
_logger.LogError(content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Selector = locator.ToString().Split("Locator@").Last();
|
||||
foreach (var element in await locator.AllAsync())
|
||||
{
|
||||
var html = await element.EvaluateAsync<string>("element => element.outerHTML");
|
||||
_logger.LogWarning(html);
|
||||
// fix if html has &
|
||||
result.Body = HttpUtility.HtmlDecode(html);
|
||||
break;
|
||||
}
|
||||
|
||||
result.IsSuccess = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,20 @@ public partial class PlaywrightWebDriver
|
|||
if (args.Direction == "down")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("document.body.scrollHeight");
|
||||
int scrollY = await page.EvaluateAsync<int>("window.screen.height");
|
||||
|
||||
// Scroll to the bottom
|
||||
// Scroll a page down
|
||||
await page.Mouse.WheelAsync(0, scrollY);
|
||||
}
|
||||
else if (args.Direction == "up")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("window.screen.height");
|
||||
|
||||
// Scroll a page up
|
||||
await page.Mouse.WheelAsync(0, -scrollY);
|
||||
}
|
||||
else if (args.Direction == "bottom")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("document.body.scrollHeight");
|
||||
|
|
@ -25,6 +33,14 @@ public partial class PlaywrightWebDriver
|
|||
// Scroll to the bottom
|
||||
await page.Mouse.WheelAsync(0, -scrollY);
|
||||
}
|
||||
else if (args.Direction == "top")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("document.body.scrollHeight");
|
||||
|
||||
// Scroll to the top
|
||||
await page.Mouse.WheelAsync(0, -scrollY);
|
||||
}
|
||||
else if (args.Direction == "left")
|
||||
{
|
||||
await page.EvaluateAsync(@"
|
||||
|
|
|
|||
Loading…
Reference in a new issue