mirror of
https://gitee.com/dromara/sa-token.git
synced 2026-02-27 16:50:24 +08:00
新增 SaOAuth2DataResolver 数据解析器,负责 Web 交互层面的数据进出
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package cn.dev33.satoken.util;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@@ -150,7 +152,42 @@ public class SaResult extends LinkedHashMap<String, Object> implements Serializa
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 写入一个 json 字符串, 连缀风格
|
||||
* @param jsonString json 字符串
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult setJsonString(String jsonString) {
|
||||
Map<String, Object> map = SaManager.getSaJsonTemplate().parseJsonToMap(jsonString);
|
||||
return setMap(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除默认属性(code、msg、data), 连缀风格
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult removeDefaultFields() {
|
||||
this.remove("code");
|
||||
this.remove("msg");
|
||||
this.remove("data");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除非默认属性(code、msg、data), 连缀风格
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult removeNonDefaultFields() {
|
||||
for (String key : this.keySet()) {
|
||||
if("code".equals(key) || "msg".equals(key) || "data".equals(key)) {
|
||||
continue;
|
||||
}
|
||||
this.remove(key);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// ============================ 静态方法快速构建 ==================================
|
||||
|
||||
@@ -180,7 +217,11 @@ public class SaResult extends LinkedHashMap<String, Object> implements Serializa
|
||||
public static SaResult get(int code, String msg, Object data) {
|
||||
return new SaResult(code, msg, data);
|
||||
}
|
||||
|
||||
|
||||
// 构建一个空的
|
||||
public static SaResult empty() {
|
||||
return new SaResult();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import com.ejlchina.okhttps.OkHttps;
|
||||
import com.pj.utils.SoMap;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import com.ejlchina.okhttps.OkHttps;
|
||||
import com.pj.utils.SoMap;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Sa-OAuth2 Client端 控制器
|
||||
@@ -21,9 +19,9 @@ import cn.dev33.satoken.util.SaResult;
|
||||
public class SaOAuthClientController {
|
||||
|
||||
// 相关参数配置
|
||||
private String clientId = "1001"; // 应用id
|
||||
private String clientSecret = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥
|
||||
private String serverUrl = "http://sa-oauth-server.com:8001"; // 服务端接口
|
||||
private final String clientId = "1001"; // 应用id
|
||||
private final String clientSecret = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥
|
||||
private final String serverUrl = "http://sa-oauth-server.com:8001"; // 服务端接口
|
||||
|
||||
// 进入首页
|
||||
@RequestMapping("/")
|
||||
@@ -52,14 +50,13 @@ public class SaOAuthClientController {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 根据openid获取其对应的userId
|
||||
SoMap data = so.getMap("data");
|
||||
long uid = getUserIdByOpenid(data.getString("openid"));
|
||||
data.set("uid", uid);
|
||||
// 根据openid获取其对应的userId
|
||||
long uid = getUserIdByOpenid(so.getString("openid"));
|
||||
so.set("uid", uid);
|
||||
|
||||
// 返回相关参数
|
||||
StpUtil.login(uid);
|
||||
return SaResult.data(data);
|
||||
return SaResult.data(so);
|
||||
}
|
||||
|
||||
// 根据 Refresh-Token 去刷新 Access-Token
|
||||
@@ -82,9 +79,8 @@ public class SaOAuthClientController {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 返回相关参数 (data=新的Access-Token )
|
||||
SoMap data = so.getMap("data");
|
||||
return SaResult.data(data);
|
||||
// 返回相关参数
|
||||
return SaResult.data(so);
|
||||
}
|
||||
|
||||
// 模式三:密码式-授权登录
|
||||
@@ -108,14 +104,13 @@ public class SaOAuthClientController {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 根据openid获取其对应的userId
|
||||
SoMap data = so.getMap("data");
|
||||
long uid = getUserIdByOpenid(data.getString("openid"));
|
||||
data.set("uid", uid);
|
||||
// 根据openid获取其对应的userId
|
||||
long uid = getUserIdByOpenid(so.getString("openid"));
|
||||
so.set("uid", uid);
|
||||
|
||||
// 返回相关参数
|
||||
StpUtil.login(uid);
|
||||
return SaResult.data(data);
|
||||
return SaResult.data(so);
|
||||
}
|
||||
|
||||
// 模式四:获取应用的 Client-Token
|
||||
@@ -137,9 +132,8 @@ public class SaOAuthClientController {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 返回相关参数 (data=新的Client-Token )
|
||||
SoMap data = so.getMap("data");
|
||||
return SaResult.data(data);
|
||||
// 返回相关参数
|
||||
return SaResult.data(so);
|
||||
}
|
||||
|
||||
// 注销登录
|
||||
@@ -166,9 +160,8 @@ public class SaOAuthClientController {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 返回相关参数 (data=获取到的资源 )
|
||||
SoMap data = so.getMap("data");
|
||||
return SaResult.data(data);
|
||||
// 返回相关参数 (data=获取到的资源 )
|
||||
return SaResult.data(so);
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 热刷新 -->
|
||||
<!--<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import cn.dev33.satoken.oauth2.dataloader.SaOAuth2DataLoader;
|
||||
import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;
|
||||
import cn.dev33.satoken.oauth2.model.SaClientModel;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
@@ -80,12 +80,13 @@ public class SaOAuth2ServerController {
|
||||
|
||||
// 模拟账号信息 (真实环境需要查询数据库获取信息)
|
||||
Map<String, Object> map = new LinkedHashMap<String, Object>();
|
||||
map.put("userId", "10008");
|
||||
map.put("nickname", "shengzhang_");
|
||||
map.put("avatar", "http://xxx.com/1.jpg");
|
||||
map.put("age", "18");
|
||||
map.put("sex", "男");
|
||||
map.put("address", "山东省 青岛市 城阳区");
|
||||
return SaResult.data(map);
|
||||
return SaResult.ok().setMap(map);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
package cn.dev33.satoken.oauth2;
|
||||
|
||||
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
|
||||
import cn.dev33.satoken.oauth2.dataloader.SaOAuth2DataLoader;
|
||||
import cn.dev33.satoken.oauth2.dataloader.SaOAuth2DataLoaderDefaultImpl;
|
||||
import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;
|
||||
import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoaderDefaultImpl;
|
||||
import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver;
|
||||
import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolverDefaultImpl;
|
||||
|
||||
/**
|
||||
* Sa-Token-OAuth2 模块 总控类
|
||||
@@ -64,4 +66,22 @@ public class SaOAuth2Manager {
|
||||
SaOAuth2Manager.dataLoader = dataLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth2 数据解析器 Bean
|
||||
*/
|
||||
private static volatile SaOAuth2DataResolver dataResolver;
|
||||
public static SaOAuth2DataResolver getDataResolver() {
|
||||
if (dataResolver == null) {
|
||||
synchronized (SaOAuth2Manager.class) {
|
||||
if (dataResolver == null) {
|
||||
setDataResolver(new SaOAuth2DataResolverDefaultImpl());
|
||||
}
|
||||
}
|
||||
}
|
||||
return dataResolver;
|
||||
}
|
||||
public static void setDataResolver(SaOAuth2DataResolver dataResolver) {
|
||||
SaOAuth2Manager.dataResolver = dataResolver;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,7 +77,22 @@ public class SaOAuth2Consts {
|
||||
public static String client_credentials = "client_credentials";
|
||||
public static String implicit = "implicit";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 所有 token 类型
|
||||
*/
|
||||
public static final class TokenType {
|
||||
// 全小写
|
||||
public static String basic = "basic";
|
||||
public static String digest = "digest";
|
||||
public static String bearer = "bearer";
|
||||
|
||||
// 首字母大写
|
||||
public static String Basic = "Basic";
|
||||
public static String Digest = "Digest";
|
||||
public static String Bearer = "Bearer";
|
||||
}
|
||||
|
||||
/** 表示OK的返回结果 */
|
||||
public static final String OK = "ok";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.dev33.satoken.oauth2.dataloader;
|
||||
package cn.dev33.satoken.oauth2.data.loader;
|
||||
|
||||
import cn.dev33.satoken.oauth2.model.SaClientModel;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.dev33.satoken.oauth2.dataloader;
|
||||
package cn.dev33.satoken.oauth2.data.loader;
|
||||
|
||||
/**
|
||||
* Sa-Token OAuth2 数据加载器 默认实现类
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2020-2099 sa-token.cc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.dev33.satoken.oauth2.data.resolver;
|
||||
|
||||
import cn.dev33.satoken.oauth2.model.AccessTokenModel;
|
||||
import cn.dev33.satoken.oauth2.model.ClientTokenModel;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sa-Token OAuth2 数据解析器,负责 Web 交互层面的数据进出:
|
||||
* <p>1、从请求中按照指定格式读取数据</p>
|
||||
* <p>2、构建数据输出格式</p>
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.39.0
|
||||
*/
|
||||
public interface SaOAuth2DataResolver {
|
||||
|
||||
|
||||
/**
|
||||
* 构建返回值: 获取 token
|
||||
* @param at token信息
|
||||
* @return /
|
||||
*/
|
||||
Map<String, Object> buildTokenReturnValue(AccessTokenModel at);
|
||||
|
||||
/**
|
||||
* 构建返回值: RefreshToken 刷新 Access-Token
|
||||
* @param at token信息
|
||||
* @return /
|
||||
*/
|
||||
default Map<String, Object> buildRefreshTokenReturnValue(AccessTokenModel at) {
|
||||
return buildTokenReturnValue(at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值: 回收 Access-Token
|
||||
* @return /
|
||||
*/
|
||||
default Map<String, Object> buildRevokeTokenReturnValue() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值: password 模式认证 获取 token
|
||||
* @param at token信息
|
||||
* @return /
|
||||
*/
|
||||
default Map<String, Object> buildPasswordReturnValue(AccessTokenModel at) {
|
||||
return buildTokenReturnValue(at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值: 凭证式 模式认证 获取 token
|
||||
* @param ct token信息
|
||||
*/
|
||||
Map<String, Object> buildClientTokenReturnValue(ClientTokenModel ct);
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2020-2099 sa-token.cc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.dev33.satoken.oauth2.data.resolver;
|
||||
|
||||
import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.TokenType;
|
||||
import cn.dev33.satoken.oauth2.model.AccessTokenModel;
|
||||
import cn.dev33.satoken.oauth2.model.ClientTokenModel;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sa-Token OAuth2 数据解析器,负责 Web 交互层面的数据进出:
|
||||
* <p>1、从请求中按照指定格式读取数据</p>
|
||||
* <p>2、构建数据输出格式</p>
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.39.0
|
||||
*/
|
||||
public class SaOAuth2DataResolverDefaultImpl implements SaOAuth2DataResolver {
|
||||
|
||||
/**
|
||||
* 构建返回值: 获取 token
|
||||
*/
|
||||
public Map<String, Object> buildTokenReturnValue(AccessTokenModel at) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("token_type", TokenType.bearer);
|
||||
map.put("access_token", at.accessToken);
|
||||
map.put("refresh_token", at.refreshToken);
|
||||
map.put("expires_in", at.getExpiresIn());
|
||||
map.put("refresh_expires_in", at.getRefreshExpiresIn());
|
||||
map.put("client_id", at.clientId);
|
||||
map.put("scope", at.scope);
|
||||
map.put("openid", at.openid);
|
||||
return SaResult.ok().setMap(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值: password 模式认证 获取 token
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> buildClientTokenReturnValue(ClientTokenModel ct) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("client_token", ct.clientToken);
|
||||
map.put("access_token", ct.clientToken); // 兼容 OAuth2 协议
|
||||
map.put("expires_in", ct.getExpiresIn());
|
||||
map.put("client_id", ct.clientId);
|
||||
map.put("scope", ct.scope);
|
||||
return SaResult.ok().setMap(map);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ public class SaOAuth2ServerProcessor {
|
||||
|
||||
// 获取变量
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
SaOAuth2Config cfg = SaOAuth2Manager.getConfig();
|
||||
|
||||
// ------------------ 路由分发 ------------------
|
||||
@@ -187,14 +186,13 @@ public class SaOAuth2ServerProcessor {
|
||||
public Object token() {
|
||||
// 获取变量
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
SaOAuth2Config cfg = SaOAuth2Manager.getConfig();
|
||||
|
||||
// 获取参数
|
||||
String authorizationValue = SaHttpBasicUtil.getAuthorizationValue();
|
||||
String clientId;
|
||||
String clientSecret;
|
||||
// gitlab回调token接口时,按照的是标准的oauth2协议的basic请求头,basic中会包含client_id和client_secret的信息
|
||||
|
||||
// gitlab 回调 token 接口时,按照的是标准的oauth2协议的basic请求头,basic中会包含client_id和client_secret的信息
|
||||
if(SaFoxUtil.isEmpty(authorizationValue)){
|
||||
clientId = req.getParamNotNull(Param.client_id);
|
||||
clientSecret = req.getParamNotNull(Param.client_secret);
|
||||
@@ -211,10 +209,10 @@ public class SaOAuth2ServerProcessor {
|
||||
oauth2Template.checkGainTokenParam(code, clientId, clientSecret, redirectUri);
|
||||
|
||||
// 构建 Access-Token
|
||||
AccessTokenModel token = oauth2Template.generateAccessToken(code);
|
||||
AccessTokenModel accessTokenModel = oauth2Template.generateAccessToken(code);
|
||||
|
||||
// 返回
|
||||
return SaResult.data(token.toLineMap());
|
||||
return SaOAuth2Manager.getDataResolver().buildTokenReturnValue(accessTokenModel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,9 +231,11 @@ public class SaOAuth2ServerProcessor {
|
||||
// 校验参数
|
||||
oauth2Template.checkRefreshTokenParam(clientId, clientSecret, refreshToken);
|
||||
|
||||
// 获取新Token返回
|
||||
Object data = oauth2Template.refreshAccessToken(refreshToken).toLineMap();
|
||||
return SaResult.data(data);
|
||||
// 获取新 Access-Token
|
||||
AccessTokenModel accessTokenModel = oauth2Template.refreshAccessToken(refreshToken);
|
||||
|
||||
// 返回
|
||||
return SaOAuth2Manager.getDataResolver().buildRefreshTokenReturnValue(accessTokenModel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +261,9 @@ public class SaOAuth2ServerProcessor {
|
||||
|
||||
// 回收 Access-Token
|
||||
oauth2Template.revokeAccessToken(accessToken);
|
||||
return SaResult.ok();
|
||||
|
||||
// 返回
|
||||
return SaOAuth2Manager.getDataResolver().buildRevokeTokenReturnValue();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,7 +273,6 @@ public class SaOAuth2ServerProcessor {
|
||||
public Object doLogin() {
|
||||
// 获取变量
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
SaOAuth2Config cfg = SaOAuth2Manager.getConfig();
|
||||
|
||||
return cfg.getDoLoginHandle().apply(req.getParamNotNull(Param.name), req.getParamNotNull(Param.pwd));
|
||||
@@ -330,7 +331,7 @@ public class SaOAuth2ServerProcessor {
|
||||
AccessTokenModel at = oauth2Template.generateAccessToken(ra, true);
|
||||
|
||||
// 6、返回 Access-Token
|
||||
return SaResult.data(at.toLineMap());
|
||||
return SaOAuth2Manager.getDataResolver().buildPasswordReturnValue(at);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,8 +341,6 @@ public class SaOAuth2ServerProcessor {
|
||||
public Object clientToken() {
|
||||
// 获取变量
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
SaOAuth2Config cfg = SaOAuth2Manager.getConfig();
|
||||
|
||||
// 获取参数
|
||||
String clientId = req.getParamNotNull(Param.client_id);
|
||||
@@ -354,11 +353,11 @@ public class SaOAuth2ServerProcessor {
|
||||
// 校验 ClientSecret
|
||||
oauth2Template.checkClientSecret(clientId, clientSecret);
|
||||
|
||||
// 返回 Client-Token
|
||||
// 生成
|
||||
ClientTokenModel ct = oauth2Template.generateClientToken(clientId, scope);
|
||||
|
||||
// 返回 Client-Token
|
||||
return SaResult.data(ct.toLineMap());
|
||||
// 返回
|
||||
return SaOAuth2Manager.getDataResolver().buildClientTokenReturnValue(ct);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ package cn.dev33.satoken.spring.oauth2;
|
||||
|
||||
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
|
||||
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
|
||||
import cn.dev33.satoken.oauth2.dataloader.SaOAuth2DataLoader;
|
||||
import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;
|
||||
import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;
|
||||
import cn.dev33.satoken.oauth2.template.SaOAuth2Template;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
Reference in New Issue
Block a user