Add account handlers.
This commit is contained in:
@@ -33,6 +33,9 @@ import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler;
|
|||||||
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
|
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler;
|
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler;
|
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler;
|
||||||
|
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountGetUserInfoHandler;
|
||||||
|
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountLogoutHandler;
|
||||||
|
import io.kamax.mxisd.http.undertow.handler.auth.v2.AccountRegisterHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler;
|
import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.identity.share.EphemeralKeyIsValidHandler;
|
import io.kamax.mxisd.http.undertow.handler.identity.share.EphemeralKeyIsValidHandler;
|
||||||
import io.kamax.mxisd.http.undertow.handler.identity.share.HelloHandler;
|
import io.kamax.mxisd.http.undertow.handler.identity.share.HelloHandler;
|
||||||
@@ -105,6 +108,11 @@ public class HttpMxisd {
|
|||||||
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
|
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
|
||||||
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
|
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
|
||||||
|
|
||||||
|
// Account endpoints
|
||||||
|
.post(AccountRegisterHandler.Path, SaneHandler.around(new AccountRegisterHandler(m.getAccMgr())))
|
||||||
|
.get(AccountGetUserInfoHandler.Path, SaneHandler.around(new AccountGetUserInfoHandler(m.getAccMgr())))
|
||||||
|
.post(AccountLogoutHandler.Path, SaneHandler.around(new AccountLogoutHandler(m.getAccMgr())))
|
||||||
|
|
||||||
// Directory endpoints
|
// Directory endpoints
|
||||||
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
|
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
package io.kamax.mxisd;
|
package io.kamax.mxisd;
|
||||||
|
|
||||||
import io.kamax.mxisd.as.AppSvcManager;
|
import io.kamax.mxisd.as.AppSvcManager;
|
||||||
|
import io.kamax.mxisd.auth.AccountManager;
|
||||||
import io.kamax.mxisd.auth.AuthManager;
|
import io.kamax.mxisd.auth.AuthManager;
|
||||||
import io.kamax.mxisd.auth.AuthProviders;
|
import io.kamax.mxisd.auth.AuthProviders;
|
||||||
import io.kamax.mxisd.backend.IdentityStoreSupplier;
|
import io.kamax.mxisd.backend.IdentityStoreSupplier;
|
||||||
@@ -85,6 +86,7 @@ public class Mxisd {
|
|||||||
private SessionManager sessMgr;
|
private SessionManager sessMgr;
|
||||||
private NotificationManager notifMgr;
|
private NotificationManager notifMgr;
|
||||||
private RegistrationManager regMgr;
|
private RegistrationManager regMgr;
|
||||||
|
private AccountManager accMgr;
|
||||||
|
|
||||||
// HS-specific classes
|
// HS-specific classes
|
||||||
private Synapse synapse;
|
private Synapse synapse;
|
||||||
@@ -124,6 +126,7 @@ public class Mxisd {
|
|||||||
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
|
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
|
||||||
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
|
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
|
||||||
asHander = new AppSvcManager(this);
|
asHander = new AppSvcManager(this);
|
||||||
|
accMgr = new AccountManager(store, resolver, getHttpClient(), cfg.getAccountConfig(), cfg.getMatrix());
|
||||||
}
|
}
|
||||||
|
|
||||||
public MxisdConfig getConfig() {
|
public MxisdConfig getConfig() {
|
||||||
@@ -194,6 +197,10 @@ public class Mxisd {
|
|||||||
return synapse;
|
return synapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccountManager getAccMgr() {
|
||||||
|
return accMgr;
|
||||||
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
build();
|
build();
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
package io.kamax.mxisd.auth;
|
package io.kamax.mxisd.auth;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import io.kamax.matrix.MatrixID;
|
||||||
import io.kamax.matrix.json.GsonUtil;
|
import io.kamax.matrix.json.GsonUtil;
|
||||||
|
import io.kamax.mxisd.config.AccountConfig;
|
||||||
|
import io.kamax.mxisd.config.MatrixConfig;
|
||||||
|
import io.kamax.mxisd.exception.BadRequestException;
|
||||||
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
import io.kamax.mxisd.exception.InvalidCredentialsException;
|
||||||
import io.kamax.mxisd.exception.NotFoundException;
|
import io.kamax.mxisd.exception.NotFoundException;
|
||||||
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
|
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
|
||||||
@@ -27,12 +31,16 @@ public class AccountManager {
|
|||||||
private final IStorage storage;
|
private final IStorage storage;
|
||||||
private final HomeserverFederationResolver resolver;
|
private final HomeserverFederationResolver resolver;
|
||||||
private final CloseableHttpClient httpClient;
|
private final CloseableHttpClient httpClient;
|
||||||
|
private final AccountConfig accountConfig;
|
||||||
|
private final MatrixConfig matrixConfig;
|
||||||
|
|
||||||
public AccountManager(IStorage storage, HomeserverFederationResolver resolver,
|
public AccountManager(IStorage storage, HomeserverFederationResolver resolver,
|
||||||
CloseableHttpClient httpClient) {
|
CloseableHttpClient httpClient, AccountConfig accountConfig, MatrixConfig matrixConfig) {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.resolver = resolver;
|
this.resolver = resolver;
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
|
this.accountConfig = accountConfig;
|
||||||
|
this.matrixConfig = matrixConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String register(OpenIdToken openIdToken) {
|
public String register(OpenIdToken openIdToken) {
|
||||||
@@ -40,6 +48,20 @@ public class AccountManager {
|
|||||||
Objects.requireNonNull(openIdToken.getTokenType(), "Missing required token type");
|
Objects.requireNonNull(openIdToken.getTokenType(), "Missing required token type");
|
||||||
Objects.requireNonNull(openIdToken.getMatrixServerName(), "Missing required matrix domain");
|
Objects.requireNonNull(openIdToken.getMatrixServerName(), "Missing required matrix domain");
|
||||||
|
|
||||||
|
String userId = getUserId(openIdToken);
|
||||||
|
|
||||||
|
String token = UUID.randomUUID().toString();
|
||||||
|
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
|
||||||
|
openIdToken.getMatrixServerName(), openIdToken.getExpiredIn(),
|
||||||
|
Instant.now().getEpochSecond(), userId, token);
|
||||||
|
storage.insertToken(account);
|
||||||
|
|
||||||
|
LOGGER.info("User {} registered", userId);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserId(OpenIdToken openIdToken) {
|
||||||
String homeserverURL = resolver.resolve(openIdToken.getMatrixServerName()).toString();
|
String homeserverURL = resolver.resolve(openIdToken.getMatrixServerName()).toString();
|
||||||
HttpGet getUserInfo = new HttpGet(
|
HttpGet getUserInfo = new HttpGet(
|
||||||
"https://" + homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
|
"https://" + homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
|
||||||
@@ -58,12 +80,30 @@ public class AccountManager {
|
|||||||
throw new InvalidCredentialsException();
|
throw new InvalidCredentialsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = UUID.randomUUID().toString();
|
checkMXID(userId);
|
||||||
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
|
return userId;
|
||||||
openIdToken.getMatrixServerName(), openIdToken.getExpiredIn(),
|
}
|
||||||
Instant.now().getEpochSecond(), userId, token);
|
|
||||||
storage.insertToken(account);
|
private void checkMXID(String userId) {
|
||||||
return token;
|
MatrixID mxid;
|
||||||
|
try {
|
||||||
|
mxid = MatrixID.asValid(userId);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
LOGGER.error("Wrong MXID: " + userId, e);
|
||||||
|
throw new BadRequestException("Wrong MXID");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getAccountConfig().isAllowOnlyTrustDomains()) {
|
||||||
|
LOGGER.info("Allow registration only for trust domain.");
|
||||||
|
if (getMatrixConfig().getDomain().equals(mxid.getDomain())) {
|
||||||
|
LOGGER.info("Allow user {} to registration", userId);
|
||||||
|
} else {
|
||||||
|
LOGGER.error("Deny user {} to registration", userId);
|
||||||
|
throw new InvalidCredentialsException();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGGER.info("Allow registration from any server.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserId(String token) {
|
public String getUserId(String token) {
|
||||||
@@ -75,4 +115,12 @@ public class AccountManager {
|
|||||||
LOGGER.info("Logout: {}", userId);
|
LOGGER.info("Logout: {}", userId);
|
||||||
storage.deleteToken(token);
|
storage.deleteToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccountConfig getAccountConfig() {
|
||||||
|
return accountConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MatrixConfig getMatrixConfig() {
|
||||||
|
return matrixConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
24
src/main/java/io/kamax/mxisd/config/AccountConfig.java
Normal file
24
src/main/java/io/kamax/mxisd/config/AccountConfig.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package io.kamax.mxisd.config;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class AccountConfig {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(DirectoryConfig.class);
|
||||||
|
|
||||||
|
private boolean allowOnlyTrustDomains = true;
|
||||||
|
|
||||||
|
public boolean isAllowOnlyTrustDomains() {
|
||||||
|
return allowOnlyTrustDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowOnlyTrustDomains(boolean allowOnlyTrustDomains) {
|
||||||
|
this.allowOnlyTrustDomains = allowOnlyTrustDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() {
|
||||||
|
log.info("--- Account config ---");
|
||||||
|
log.info("Allow registration only for trust domain: {}", isAllowOnlyTrustDomains());
|
||||||
|
}
|
||||||
|
}
|
@@ -92,6 +92,7 @@ public class MxisdConfig {
|
|||||||
private AppServiceConfig appsvc = new AppServiceConfig();
|
private AppServiceConfig appsvc = new AppServiceConfig();
|
||||||
private AuthenticationConfig auth = new AuthenticationConfig();
|
private AuthenticationConfig auth = new AuthenticationConfig();
|
||||||
private DirectoryConfig directory = new DirectoryConfig();
|
private DirectoryConfig directory = new DirectoryConfig();
|
||||||
|
private AccountConfig accountConfig = new AccountConfig();
|
||||||
private Dns dns = new Dns();
|
private Dns dns = new Dns();
|
||||||
private ExecConfig exec = new ExecConfig();
|
private ExecConfig exec = new ExecConfig();
|
||||||
private FirebaseConfig firebase = new FirebaseConfig();
|
private FirebaseConfig firebase = new FirebaseConfig();
|
||||||
@@ -131,6 +132,14 @@ public class MxisdConfig {
|
|||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccountConfig getAccountConfig() {
|
||||||
|
return accountConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccountConfig(AccountConfig accountConfig) {
|
||||||
|
this.accountConfig = accountConfig;
|
||||||
|
}
|
||||||
|
|
||||||
public DirectoryConfig getDirectory() {
|
public DirectoryConfig getDirectory() {
|
||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
@@ -330,6 +339,7 @@ public class MxisdConfig {
|
|||||||
|
|
||||||
getAppsvc().build();
|
getAppsvc().build();
|
||||||
getAuth().build();
|
getAuth().build();
|
||||||
|
getAccountConfig().build();
|
||||||
getDirectory().build();
|
getDirectory().build();
|
||||||
getExec().build();
|
getExec().build();
|
||||||
getFirebase().build();
|
getFirebase().build();
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2018 Kamax Sarl
|
||||||
|
*
|
||||||
|
* https://www.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.http.undertow.handler.auth.v2;
|
||||||
|
|
||||||
|
import io.kamax.matrix.json.GsonUtil;
|
||||||
|
import io.kamax.mxisd.auth.AccountManager;
|
||||||
|
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class AccountGetUserInfoHandler extends BasicHttpHandler {
|
||||||
|
|
||||||
|
public static final String Path = "/_matrix/identity/v2/account";
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AccountGetUserInfoHandler.class);
|
||||||
|
|
||||||
|
private final AccountManager accountManager;
|
||||||
|
|
||||||
|
public AccountGetUserInfoHandler(AccountManager accountManager) {
|
||||||
|
this.accountManager = accountManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(HttpServerExchange exchange) {
|
||||||
|
String token = getQueryParameter(exchange, "access_token");
|
||||||
|
if (token == null) {
|
||||||
|
token = getAccessToken(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = accountManager.getUserId(token);
|
||||||
|
|
||||||
|
respond(exchange, GsonUtil.makeObj("user_id", userId));
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2018 Kamax Sarl
|
||||||
|
*
|
||||||
|
* https://www.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.http.undertow.handler.auth.v2;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.auth.AccountManager;
|
||||||
|
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class AccountLogoutHandler extends BasicHttpHandler {
|
||||||
|
|
||||||
|
public static final String Path = "/_matrix/identity/v2/account";
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AccountLogoutHandler.class);
|
||||||
|
|
||||||
|
private final AccountManager accountManager;
|
||||||
|
|
||||||
|
public AccountLogoutHandler(AccountManager accountManager) {
|
||||||
|
this.accountManager = accountManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(HttpServerExchange exchange) {
|
||||||
|
String token = getQueryParameter(exchange, "access_token");
|
||||||
|
if (token == null) {
|
||||||
|
token = getAccessToken(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
accountManager.logout(token);
|
||||||
|
|
||||||
|
respondJson(exchange, "{}");
|
||||||
|
}
|
||||||
|
}
|
@@ -20,13 +20,9 @@
|
|||||||
|
|
||||||
package io.kamax.mxisd.http.undertow.handler.auth.v2;
|
package io.kamax.mxisd.http.undertow.handler.auth.v2;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import io.kamax.matrix.json.GsonUtil;
|
import io.kamax.matrix.json.GsonUtil;
|
||||||
import io.kamax.mxisd.auth.AuthManager;
|
import io.kamax.mxisd.auth.AccountManager;
|
||||||
import io.kamax.mxisd.auth.UserAuthResult;
|
import io.kamax.mxisd.auth.OpenIdToken;
|
||||||
import io.kamax.mxisd.exception.JsonMemberNotFoundException;
|
|
||||||
import io.kamax.mxisd.http.io.CredentialsValidationResponse;
|
|
||||||
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
|
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
|
||||||
import io.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -38,33 +34,16 @@ public class AccountRegisterHandler extends BasicHttpHandler {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(AccountRegisterHandler.class);
|
private static final Logger log = LoggerFactory.getLogger(AccountRegisterHandler.class);
|
||||||
|
|
||||||
private AuthManager mgr;
|
private final AccountManager accountManager;
|
||||||
|
|
||||||
public AccountRegisterHandler(AuthManager mgr) {
|
public AccountRegisterHandler(AccountManager accountManager) {
|
||||||
this.mgr = mgr;
|
this.accountManager = accountManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleRequest(HttpServerExchange exchange) {
|
public void handleRequest(HttpServerExchange exchange) {
|
||||||
JsonObject authData = parseJsonObject(exchange, "user");
|
OpenIdToken openIdToken = parseJsonTo(exchange, OpenIdToken.class);
|
||||||
if (!authData.has("id") || !authData.has("password")) {
|
String token = accountManager.register(openIdToken);
|
||||||
throw new JsonMemberNotFoundException("Missing id or password keys");
|
respond(exchange, GsonUtil.makeObj("token", token));
|
||||||
}
|
|
||||||
|
|
||||||
String id = GsonUtil.getStringOrThrow(authData, "id");
|
|
||||||
log.info("Requested to check credentials for {}", id);
|
|
||||||
String password = GsonUtil.getStringOrThrow(authData, "password");
|
|
||||||
|
|
||||||
UserAuthResult result = mgr.authenticate(id, password);
|
|
||||||
CredentialsValidationResponse response = new CredentialsValidationResponse(result.isSuccess());
|
|
||||||
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
response.setDisplayName(result.getDisplayName());
|
|
||||||
response.getProfile().setThreePids(result.getThreePids());
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonElement authObj = GsonUtil.get().toJsonTree(response);
|
|
||||||
respond(exchange, GsonUtil.makeObj("auth", authObj));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user