⚡优化登录
This commit is contained in:
parent
ea3fa0e0d6
commit
52240ab6a8
@ -5,8 +5,6 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Principal;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
|
|
||||||
namespace Infrastructure
|
namespace Infrastructure
|
||||||
{
|
{
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="UAParser" Version="3.1.47" />
|
<PackageReference Include="UAParser" Version="3.1.47" />
|
||||||
|
<PackageReference Include="IPTools.China" Version="1.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Infrastructure.Extensions;
|
using Infrastructure.Extensions;
|
||||||
|
using IPTools.Core;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -197,14 +198,24 @@ namespace Infrastructure.WebExtensins
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据IP获取地理位置
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetIpInfo(string IP)
|
||||||
|
{
|
||||||
|
var ipInfo = IpTool.Search(IP);
|
||||||
|
return ipInfo?.Province + "-" + ipInfo?.City + "-" + ipInfo?.NetworkOperator;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置请求参数
|
/// 设置请求参数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reqMethod"></param>
|
/// <param name="reqMethod"></param>
|
||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
public static string GetRequestValue(this HttpContext context,string reqMethod)
|
public static string GetRequestValue(this HttpContext context, string reqMethod)
|
||||||
{
|
{
|
||||||
string param= string.Empty;
|
string param = string.Empty;
|
||||||
|
|
||||||
if (HttpMethods.IsPost(reqMethod) || HttpMethods.IsPut(reqMethod) || HttpMethods.IsDelete(reqMethod))
|
if (HttpMethods.IsPost(reqMethod) || HttpMethods.IsPut(reqMethod) || HttpMethods.IsDelete(reqMethod))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -195,6 +195,7 @@ Vue 版前端技术栈 :基于 vue2.x/vue3.x/uniapp、vuex、vue-router 、vue
|
|||||||
- 👉SqlSugar:[SqlSugar](https://gitee.com/dotnetchina/SqlSugar)
|
- 👉SqlSugar:[SqlSugar](https://gitee.com/dotnetchina/SqlSugar)
|
||||||
- 👉vue-element-admin:[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
- 👉vue-element-admin:[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||||
- 👉Meiam.System:[Meiam.System](https://github.com/91270/Meiam.System)
|
- 👉Meiam.System:[Meiam.System](https://github.com/91270/Meiam.System)
|
||||||
|
- 👉Furion:[Furion](https://gitee.com/dotnetchina/Furion)
|
||||||
|
|
||||||
## 🎀 捐赠
|
## 🎀 捐赠
|
||||||
|
|
||||||
|
|||||||
@ -79,13 +79,14 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||||||
{
|
{
|
||||||
return ToResponse(ResultCode.LOGIN_ERROR, $"你的账号已被锁,剩余{Math.Round(ts.TotalMinutes, 0)}分钟");
|
return ToResponse(ResultCode.LOGIN_ERROR, $"你的账号已被锁,剩余{Math.Round(ts.TotalMinutes, 0)}分钟");
|
||||||
}
|
}
|
||||||
var user = sysLoginService.Login(loginBody, RecordLogInfo(httpContextAccessor.HttpContext));
|
string location = HttpContextExtension.GetIpInfo(loginBody.LoginIP);
|
||||||
|
var user = sysLoginService.Login(loginBody, new SysLogininfor() { LoginLocation = location });
|
||||||
|
|
||||||
List<SysRole> roles = roleService.SelectUserRoleListByUserId(user.UserId);
|
List<SysRole> roles = roleService.SelectUserRoleListByUserId(user.UserId);
|
||||||
//权限集合 eg *:*:*,system:user:list
|
//权限集合 eg *:*:*,system:user:list
|
||||||
List<string> permissions = permissionService.GetMenuPermission(user);
|
List<string> permissions = permissionService.GetMenuPermission(user);
|
||||||
|
|
||||||
LoginUser loginUser = new(user, roles);
|
LoginUser loginUser = new(user, roles.Adapt<List<Roles>>());
|
||||||
CacheService.SetUserPerms(GlobalConstant.UserPermKEY + user.UserId, permissions);
|
CacheService.SetUserPerms(GlobalConstant.UserPermKEY + user.UserId, permissions);
|
||||||
return SUCCESS(JwtUtil.GenerateJwtToken(JwtUtil.AddClaims(loginUser)));
|
return SUCCESS(JwtUtil.GenerateJwtToken(JwtUtil.AddClaims(loginUser)));
|
||||||
}
|
}
|
||||||
@ -128,8 +129,6 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||||||
List<string> permissions = permissionService.GetMenuPermission(user);
|
List<string> permissions = permissionService.GetMenuPermission(user);
|
||||||
user.WelcomeContent = GlobalConstant.WelcomeMessages[new Random().Next(0, GlobalConstant.WelcomeMessages.Length)];
|
user.WelcomeContent = GlobalConstant.WelcomeMessages[new Random().Next(0, GlobalConstant.WelcomeMessages.Length)];
|
||||||
|
|
||||||
//LoginUser loginUser = new(user, roleService.SelectUserRoleListByUserId(user.UserId), permissions);
|
|
||||||
//var token = JwtUtil.GenerateJwtToken(JwtUtil.AddClaims(loginUser), optionSettings.JwtSettings);
|
|
||||||
return SUCCESS(new { user, roles, permissions });
|
return SUCCESS(new { user, roles, permissions });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,29 +163,6 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||||||
return SUCCESS(obj);
|
return SUCCESS(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 记录用户登陆信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public SysLogininfor RecordLogInfo(HttpContext context)
|
|
||||||
{
|
|
||||||
var ipAddr = context.GetClientUserIp();
|
|
||||||
var ip_info = IpTool.Search(ipAddr);
|
|
||||||
ClientInfo clientInfo = context.GetClientInfo();
|
|
||||||
SysLogininfor sysLogininfor = new()
|
|
||||||
{
|
|
||||||
Browser = clientInfo.ToString(),
|
|
||||||
Os = clientInfo.OS.ToString(),
|
|
||||||
Ipaddr = ipAddr,
|
|
||||||
UserName = context.GetName(),
|
|
||||||
LoginLocation = ip_info?.Province + "-" + ip_info?.City
|
|
||||||
};
|
|
||||||
|
|
||||||
return sysLogininfor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册
|
/// 注册
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using Infrastructure.Extensions;
|
using IPTools.Core;
|
||||||
using IPTools.Core;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
@ -26,9 +25,7 @@ namespace ZR.Admin.WebApi.Filters
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||||
{
|
{
|
||||||
ApiResult response = new();
|
string msg = string.Empty;
|
||||||
response.Code = (int)ResultCode.PARAM_ERROR;
|
|
||||||
|
|
||||||
var values = context.ModelState.Values;
|
var values = context.ModelState.Values;
|
||||||
foreach (var item in values)
|
foreach (var item in values)
|
||||||
{
|
{
|
||||||
@ -38,17 +35,22 @@ namespace ZR.Admin.WebApi.Filters
|
|||||||
{
|
{
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
if (!string.IsNullOrEmpty(response.Msg))
|
if (!string.IsNullOrEmpty(msg))
|
||||||
{
|
{
|
||||||
response.Msg += " | ";
|
msg += " | ";
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Msg += err.ErrorMessage;
|
msg += err.ErrorMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!string.IsNullOrEmpty(response.Msg))
|
if (!string.IsNullOrEmpty(msg))
|
||||||
{
|
{
|
||||||
logger.Info($"请求参数错误,{response.Msg}");
|
logger.Info($"请求参数错误,{msg}");
|
||||||
|
ApiResult response = new()
|
||||||
|
{
|
||||||
|
Code = (int)ResultCode.PARAM_ERROR,
|
||||||
|
Msg = msg
|
||||||
|
};
|
||||||
context.Result = new JsonResult(response);
|
context.Result = new JsonResult(response);
|
||||||
}
|
}
|
||||||
return base.OnActionExecutionAsync(context, next);
|
return base.OnActionExecutionAsync(context, next);
|
||||||
@ -98,7 +100,7 @@ namespace ZR.Admin.WebApi.Filters
|
|||||||
OperUrl = HttpContextExtension.GetRequestUrl(context.HttpContext),
|
OperUrl = HttpContextExtension.GetRequestUrl(context.HttpContext),
|
||||||
RequestMethod = method,
|
RequestMethod = method,
|
||||||
JsonResult = jsonResult,
|
JsonResult = jsonResult,
|
||||||
OperLocation = ip_info.Province + " " + ip_info.City,
|
OperLocation = HttpContextExtension.GetIpInfo(ip),
|
||||||
Method = controller + "." + action + "()",
|
Method = controller + "." + action + "()",
|
||||||
//Elapsed = _stopwatch.ElapsedMilliseconds,
|
//Elapsed = _stopwatch.ElapsedMilliseconds,
|
||||||
OperTime = DateTime.Now,
|
OperTime = DateTime.Now,
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using JinianNet.JNTemplate;
|
||||||
|
using JinianNet.JNTemplate.Nodes;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using ZR.Admin.WebApi.Hubs;
|
||||||
using ZR.Model.System.Dto;
|
using ZR.Model.System.Dto;
|
||||||
|
|
||||||
namespace ZR.Admin.WebApi.Framework
|
namespace ZR.Admin.WebApi.Framework
|
||||||
@ -122,7 +127,8 @@ namespace ZR.Admin.WebApi.Framework
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IEnumerable<Claim> claims = jwtSecurityToken.Claims;
|
if (jwtSecurityToken == null) return null;
|
||||||
|
IEnumerable<Claim> claims = jwtSecurityToken?.Claims;
|
||||||
LoginUser loginUser = null;
|
LoginUser loginUser = null;
|
||||||
|
|
||||||
var userData = claims.FirstOrDefault(x => x.Type == ClaimTypes.UserData)?.Value;
|
var userData = claims.FirstOrDefault(x => x.Type == ClaimTypes.UserData)?.Value;
|
||||||
@ -131,7 +137,21 @@ namespace ZR.Admin.WebApi.Framework
|
|||||||
loginUser = JsonConvert.DeserializeObject<LoginUser>(userData);
|
loginUser = JsonConvert.DeserializeObject<LoginUser>(userData);
|
||||||
loginUser.ExpireTime = jwtSecurityToken.ValidTo;
|
loginUser.ExpireTime = jwtSecurityToken.ValidTo;
|
||||||
}
|
}
|
||||||
//Console.WriteLine("jwt到期时间:" + validTo);
|
//var nowTime = DateTime.UtcNow;
|
||||||
|
//TimeSpan ts = loginUser.ExpireTime - nowTime;
|
||||||
|
|
||||||
|
//Console.WriteLine("jwt到期时间:" + loginUser.ExpireTime);
|
||||||
|
//Console.WriteLine("nowTime" + nowTime + ",相隔" + ts.TotalSeconds);
|
||||||
|
|
||||||
|
//if (loginUser != null && ts.TotalSeconds <= 30)
|
||||||
|
//{
|
||||||
|
// var newToken = GenerateJwtToken(AddClaims(loginUser));
|
||||||
|
// var CK = "token_" + loginUser.UserId;
|
||||||
|
// if (!CacheHelper.Exists(CK))
|
||||||
|
// {
|
||||||
|
// CacheHelper.SetCache(CK, newToken);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
return loginUser;
|
return loginUser;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ using ZR.Admin.WebApi.Framework;
|
|||||||
using ZR.Admin.WebApi.Hubs;
|
using ZR.Admin.WebApi.Hubs;
|
||||||
using ZR.Admin.WebApi.Middleware;
|
using ZR.Admin.WebApi.Middleware;
|
||||||
using ZR.Common.Cache;
|
using ZR.Common.Cache;
|
||||||
|
using ZR.Model.System.Dto;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@ -54,11 +55,12 @@ builder.Services.AddAuthentication(options =>
|
|||||||
// 如果过期,把过期信息添加到头部
|
// 如果过期,把过期信息添加到头部
|
||||||
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("jwt过期了");
|
||||||
context.Response.Headers.Add("Token-Expired", "true");
|
context.Response.Headers.Add("Token-Expired", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.7" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
||||||
<PackageReference Include="IPTools.China" Version="1.6.0" />
|
|
||||||
<PackageReference Include="NLog" Version="5.2.3" />
|
<PackageReference Include="NLog" Version="5.2.3" />
|
||||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.3" />
|
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.3" />
|
||||||
<PackageReference Include="Mapster" Version="7.3.0" />
|
<PackageReference Include="Mapster" Version="7.3.0" />
|
||||||
|
|||||||
@ -17,7 +17,7 @@ namespace ZR.Model.System.Dto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 角色集合(数据权限过滤使用)
|
/// 角色集合(数据权限过滤使用)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SysRole> Roles { get; set; }
|
public List<Roles> Roles { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Jwt过期时间
|
/// Jwt过期时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -30,7 +30,7 @@ namespace ZR.Model.System.Dto
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginUser(SysUser user, List<SysRole> roles)
|
public LoginUser(SysUser user, List<Roles> roles)
|
||||||
{
|
{
|
||||||
UserId = user.UserId;
|
UserId = user.UserId;
|
||||||
UserName = user.UserName;
|
UserName = user.UserName;
|
||||||
@ -39,4 +39,11 @@ namespace ZR.Model.System.Dto
|
|||||||
RoleIds = roles.Select(f => f.RoleKey).ToList();
|
RoleIds = roles.Select(f => f.RoleKey).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Roles
|
||||||
|
{
|
||||||
|
public long RoleId { get; set; }
|
||||||
|
public string RoleKey { get; set; }
|
||||||
|
public int DataScope { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
using System;
|
using ZR.Model;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using ZR.Model;
|
|
||||||
using ZR.Model.System.Dto;
|
|
||||||
using ZR.Model.System;
|
using ZR.Model.System;
|
||||||
|
using ZR.Model.System.Dto;
|
||||||
|
|
||||||
namespace ZR.Service.System.IService
|
namespace ZR.Service.System.IService
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
using Infrastructure;
|
using Infrastructure;
|
||||||
using Infrastructure.Attribute;
|
using Infrastructure.Attribute;
|
||||||
using Infrastructure.Extensions;
|
using Infrastructure.Extensions;
|
||||||
|
using Infrastructure.WebExtensins;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using System;
|
using System;
|
||||||
|
using UAParser;
|
||||||
using ZR.Model;
|
using ZR.Model;
|
||||||
using ZR.Model.System;
|
using ZR.Model.System;
|
||||||
using ZR.Model.System.Dto;
|
using ZR.Model.System.Dto;
|
||||||
@ -15,13 +18,15 @@ namespace ZR.Service.System
|
|||||||
/// 登录
|
/// 登录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AppService(ServiceType = typeof(ISysLoginService), ServiceLifetime = LifeTime.Transient)]
|
[AppService(ServiceType = typeof(ISysLoginService), ServiceLifetime = LifeTime.Transient)]
|
||||||
public class SysLoginService: BaseService<SysLogininfor>, ISysLoginService
|
public class SysLoginService : BaseService<SysLogininfor>, ISysLoginService
|
||||||
{
|
{
|
||||||
private readonly ISysUserService SysUserService;
|
private readonly ISysUserService SysUserService;
|
||||||
|
private readonly IHttpContextAccessor httpContextAccessor;
|
||||||
|
|
||||||
public SysLoginService(ISysUserService sysUserService)
|
public SysLoginService(ISysUserService sysUserService, IHttpContextAccessor httpContextAccessor)
|
||||||
{
|
{
|
||||||
SysUserService = sysUserService;
|
SysUserService = sysUserService;
|
||||||
|
this.httpContextAccessor = httpContextAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -35,17 +40,21 @@ namespace ZR.Service.System
|
|||||||
{
|
{
|
||||||
loginBody.Password = NETCore.Encrypt.EncryptProvider.Md5(loginBody.Password);
|
loginBody.Password = NETCore.Encrypt.EncryptProvider.Md5(loginBody.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
SysUser user = SysUserService.Login(loginBody);
|
SysUser user = SysUserService.Login(loginBody);
|
||||||
logininfor.UserName = loginBody.Username;
|
logininfor.UserName = loginBody.Username;
|
||||||
logininfor.Status = "1";
|
logininfor.Status = "1";
|
||||||
logininfor.LoginTime = DateTime.Now;
|
logininfor.LoginTime = DateTime.Now;
|
||||||
|
logininfor.Ipaddr = loginBody.LoginIP;
|
||||||
|
|
||||||
|
ClientInfo clientInfo = httpContextAccessor.HttpContext.GetClientInfo();
|
||||||
|
logininfor.Browser = clientInfo.ToString();
|
||||||
|
logininfor.Os = clientInfo.OS.ToString();
|
||||||
|
|
||||||
if (user == null || user.UserId <= 0)
|
if (user == null || user.UserId <= 0)
|
||||||
{
|
{
|
||||||
logininfor.Msg = "用户名或密码错误";
|
logininfor.Msg = "用户名或密码错误";
|
||||||
AddLoginInfo(logininfor);
|
AddLoginInfo(logininfor);
|
||||||
throw new CustomException(ResultCode.LOGIN_ERROR ,logininfor.Msg);
|
throw new CustomException(ResultCode.LOGIN_ERROR, logininfor.Msg);
|
||||||
}
|
}
|
||||||
if (user.Status == 1)
|
if (user.Status == 1)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user