Merge pull request #716 from Qtoss-AI/master

WebDriver improvement
This commit is contained in:
Haiping 2024-11-01 11:43:49 -05:00 committed by GitHub
commit fd0897019b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 331 additions and 145 deletions

View file

@ -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" />

View file

@ -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()

View file

@ -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; }

View file

@ -1,5 +1,8 @@
using System.Diagnostics;
namespace BotSharp.Abstraction.Browsing.Models;
[DebuggerStepThrough]
public class ElementLocatingArgs
{
[JsonPropertyName("match_rule")]

View file

@ -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!;

View file

@ -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!;

View file

@ -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}";
}
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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

View file

@ -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);
}

View file

@ -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; }
}

View file

@ -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);

View file

@ -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;

View file

@ -7,4 +7,5 @@ public class AccountSetting
/// </summary>
public bool NewUserVerification { get; set; }
public string[] AllowMultipleDeviceLoginUserIds { get; set; } = [];
public bool CreateUserAutomatically { get; set; } = true;
}

View file

@ -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)

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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")]

View file

@ -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();
}
}

View file

@ -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
};
}
}

View file

@ -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
};
}
}

View file

@ -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
};
}
}

View file

@ -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>

View file

@ -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,
};
}
}

View file

@ -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);
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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(@"