mirror of
https://github.com/hubHarmony/servii-backend.git
synced 2024-11-18 05:50:31 +00:00
[+] New auth system
Merge pull request #34 from hubHarmony/plugin-management-system
This commit is contained in:
commit
dd5c2a4933
2
.gitignore
vendored
2
.gitignore
vendored
@ -163,6 +163,8 @@ cython_debug/
|
|||||||
|
|
||||||
/__pycache__/
|
/__pycache__/
|
||||||
/servers/bukkit/
|
/servers/bukkit/
|
||||||
|
/server/fabric/
|
||||||
|
/servers/forge/
|
||||||
/servers/paper/
|
/servers/paper/
|
||||||
/servers/spigot/
|
/servers/spigot/
|
||||||
/users/
|
/users/
|
||||||
|
@ -107,6 +107,8 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const uploadForm = document.getElementById('uploadForm');
|
const uploadForm = document.getElementById('uploadForm');
|
||||||
const messageDiv = document.getElementById('message');
|
const messageDiv = document.getElementById('message');
|
||||||
|
const token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImUwM2E2ODg3YWU3ZjNkMTAyNzNjNjRiMDU3ZTY1MzE1MWUyOTBiNzIiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiSXR6IFNlbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NLOElVZHlzcW5kZkxxNFc5ZWlRNlpjTFpkbUVDX29UNXBVaURGQ2gzY2VDZTZXSGxvWD1zOTYtYyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9zZXJ2aS1lNjcwNSIsImF1ZCI6InNlcnZpLWU2NzA1IiwiYXV0aF90aW1lIjoxNzI2ODM5NTUwLCJ1c2VyX2lkIjoiTXBrYkRNT084UFFkZFFnQjVWZ0JRZFRNV0Y1MyIsInN1YiI6Ik1wa2JETU9POFBRZGRRZ0I1VmdCUWRUTVdGNTMiLCJpYXQiOjE3MjY4Mzk1NTAsImV4cCI6MTcyNjg0MzE1MCwiZW1haWwiOiJ0ZWNobm9wcm9kMjU0NTg1NjVAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyIxMTQ0Mzk0NjEyOTM5OTE1NzU5MTgiXSwiZW1haWwiOlsidGVjaG5vcHJvZDI1NDU4NTY1QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6Imdvb2dsZS5jb20ifX0.Aa3mdAmOzYET_QsGk2-QKxLGhxtXGfyAcTRnnM6cPGx0UJeSoQ-EhMIgK7HDiLVni_eMHbnwMSeEXDHEpsCWosm6e3e96zwMU3GXI1nowcnZ3CYTDH8jDCs2-6_ODomZtT2S1Lp3fD7IoSD4tDGFdo9kZNyuFGApTHhFHNAyHvfBqGL_c0c71Gfh-6ywl5C8nc07YPVbYGJu6GrS28L1vOjRSkl89Xm7o6atf38YWYWwg84QsrugRlF7Nz6yZJf7cjRY5x2guilqxrWVCWhlLiCMqFhe4oIW3BL7s3AfUC6U7DvlTyGwZJoN3fUr7V1Q5xloqSz7dcexRe1YkXXrCA";
|
||||||
|
|
||||||
|
|
||||||
// File Upload functionality
|
// File Upload functionality
|
||||||
uploadForm.addEventListener('submit', async event => {
|
uploadForm.addEventListener('submit', async event => {
|
||||||
@ -129,6 +131,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:3000/Upload', {
|
const response = await fetch('http://localhost:3000/Upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: {'SST': token},
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -157,7 +160,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
button.addEventListener('click', async event => {
|
button.addEventListener('click', async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const action = button.dataset.action;
|
const action = button.dataset.action;
|
||||||
const token = "MpkbDMOO8PQddQgB5VgBQdTMWF53";
|
//const token = "dhmNGJYaVzNkKWgMAEOoAjaPWdc2";
|
||||||
|
const token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImUwM2E2ODg3YWU3ZjNkMTAyNzNjNjRiMDU3ZTY1MzE1MWUyOTBiNzIiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiSXR6IFNlbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NLOElVZHlzcW5kZkxxNFc5ZWlRNlpjTFpkbUVDX29UNXBVaURGQ2gzY2VDZTZXSGxvWD1zOTYtYyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9zZXJ2aS1lNjcwNSIsImF1ZCI6InNlcnZpLWU2NzA1IiwiYXV0aF90aW1lIjoxNzI2ODM5NTUwLCJ1c2VyX2lkIjoiTXBrYkRNT084UFFkZFFnQjVWZ0JRZFRNV0Y1MyIsInN1YiI6Ik1wa2JETU9POFBRZGRRZ0I1VmdCUWRUTVdGNTMiLCJpYXQiOjE3MjY4Mzk1NTAsImV4cCI6MTcyNjg0MzE1MCwiZW1haWwiOiJ0ZWNobm9wcm9kMjU0NTg1NjVAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyIxMTQ0Mzk0NjEyOTM5OTE1NzU5MTgiXSwiZW1haWwiOlsidGVjaG5vcHJvZDI1NDU4NTY1QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6Imdvb2dsZS5jb20ifX0.Aa3mdAmOzYET_QsGk2-QKxLGhxtXGfyAcTRnnM6cPGx0UJeSoQ-EhMIgK7HDiLVni_eMHbnwMSeEXDHEpsCWosm6e3e96zwMU3GXI1nowcnZ3CYTDH8jDCs2-6_ODomZtT2S1Lp3fD7IoSD4tDGFdo9kZNyuFGApTHhFHNAyHvfBqGL_c0c71Gfh-6ywl5C8nc07YPVbYGJu6GrS28L1vOjRSkl89Xm7o6atf38YWYWwg84QsrugRlF7Nz6yZJf7cjRY5x2guilqxrWVCWhlLiCMqFhe4oIW3BL7s3AfUC6U7DvlTyGwZJoN3fUr7V1Q5xloqSz7dcexRe1YkXXrCA";
|
||||||
const framework = document.getElementById('serverFramework').value;
|
const framework = document.getElementById('serverFramework').value;
|
||||||
const subdomain = document.getElementById('subdomain').value;
|
const subdomain = document.getElementById('subdomain').value;
|
||||||
const email = document.getElementById('accountEmail').value;
|
const email = document.getElementById('accountEmail').value;
|
||||||
@ -172,58 +176,59 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case 'FetchServers':
|
case 'FetchServers':
|
||||||
data = {token};
|
data = {};
|
||||||
break;
|
break;
|
||||||
case 'FetchLogs':
|
case 'FetchLogs':
|
||||||
data = {token, name};
|
data = {name};
|
||||||
break;
|
break;
|
||||||
case 'FetchPlayersStatus':
|
case 'FetchPlayersStatus':
|
||||||
data = {token, name};
|
data = { name};
|
||||||
break;
|
break;
|
||||||
case 'FetchDirContent':
|
case 'FetchDirContent':
|
||||||
data = {token, name};
|
data = { name};
|
||||||
break;
|
break;
|
||||||
case 'AccountCreate':
|
case 'AccountCreate':
|
||||||
data = {email, port, token};
|
data = {email, port, };
|
||||||
break;
|
break;
|
||||||
case 'AccountDelete':
|
case 'AccountDelete':
|
||||||
data = {subdomain, port, token};
|
data = {subdomain, port, };
|
||||||
break;
|
break;
|
||||||
case 'ServerCreate':
|
case 'ServerCreate':
|
||||||
data = {port, name, version, token, framework};
|
data = {port, name, version, framework};
|
||||||
break;
|
break;
|
||||||
case 'ServerDelete':
|
case 'ServerDelete':
|
||||||
data = {port, name, token};
|
data = {port, name, };
|
||||||
break;
|
break;
|
||||||
case 'ServerRun':
|
case 'ServerRun':
|
||||||
data = {port, name, token};
|
data = {port, name, };
|
||||||
break;
|
break;
|
||||||
case 'ServerStop':
|
case 'ServerStop':
|
||||||
data = {port, name, token};
|
data = {port, name, };
|
||||||
break;
|
break;
|
||||||
case 'UpdateProperties':
|
case 'UpdateProperties':
|
||||||
data = {port, name, props, value, token};
|
data = {port, name, props, value, };
|
||||||
break;
|
break;
|
||||||
case 'Command':
|
case 'Command':
|
||||||
data = {port, name, command, token};
|
data = {port, name, command, };
|
||||||
break;
|
break;
|
||||||
case 'SetSubdomain':
|
case 'SetSubdomain':
|
||||||
data = {token, subdomain};
|
data = {subdomain};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRequest(action, data)
|
sendRequest(action, data, token)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => alert(`Response: ${data}`))
|
.then(data => alert(`Response: ${data}`))
|
||||||
.catch(error => console.error('Error:', error));
|
.catch(error => console.error('Error:', error));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function sendRequest(endpoint, payload) {
|
function sendRequest(endpoint, payload, token) {
|
||||||
return fetch(`http://localhost:3000/${endpoint}`, {
|
return fetch(`http://localhost:3000/${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'SST': token,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
29
app.py
29
app.py
@ -55,11 +55,8 @@ TODO : replace 53 by the given statement.
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def authenticate_request(data: dict):
|
def authenticate_request(token: str):
|
||||||
if 'token' not in data:
|
valid, user_id = firebase_manager.verify_jwt_token(token)
|
||||||
raise Exception("Missing 'token' in request body. The API doesn't support anonymous access anymore.")
|
|
||||||
else:
|
|
||||||
valid, user_id = True, data['token']
|
|
||||||
if not valid:
|
if not valid:
|
||||||
raise Exception("Invalid JWT token.")
|
raise Exception("Invalid JWT token.")
|
||||||
else:
|
else:
|
||||||
@ -71,12 +68,12 @@ def authenticate_request(data: dict):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def parse_and_validate_request(parameters: list[str]) -> Union[list[str], None]:
|
def parse_and_validate_request(parameters: list[str], token: str) -> Union[list[str], None]:
|
||||||
fn_args = []
|
fn_args = []
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
raise Exception("Empty request body.")
|
raise Exception("Empty request body.")
|
||||||
user = authenticate_request(data)
|
user = authenticate_request(token)
|
||||||
data['user'] = user
|
data['user'] = user
|
||||||
for name in parameters:
|
for name in parameters:
|
||||||
if name not in data:
|
if name not in data:
|
||||||
@ -115,7 +112,10 @@ def dynamic_route_handler(path):
|
|||||||
for param in sig.parameters.values():
|
for param in sig.parameters.values():
|
||||||
parameters.append(param.name)
|
parameters.append(param.name)
|
||||||
try:
|
try:
|
||||||
mapped_parameters = parse_and_validate_request(parameters)
|
token: str or None = request.headers.get('SST')
|
||||||
|
if not token:
|
||||||
|
return generic_response_maker(http.HTTPStatus.BAD_REQUEST, "No provided token.")
|
||||||
|
mapped_parameters = parse_and_validate_request(parameters, token)
|
||||||
if mapped_parameters is None:
|
if mapped_parameters is None:
|
||||||
return generic_response_maker(http.HTTPStatus.BAD_REQUEST)
|
return generic_response_maker(http.HTTPStatus.BAD_REQUEST)
|
||||||
status, message = route_fn(*mapped_parameters)
|
status, message = route_fn(*mapped_parameters)
|
||||||
@ -130,7 +130,7 @@ def dynamic_route_handler(path):
|
|||||||
@apiBP.route('/Upload', methods=['POST'])
|
@apiBP.route('/Upload', methods=['POST'])
|
||||||
def upload():
|
def upload():
|
||||||
form = request.form
|
form = request.form
|
||||||
token: str or None = form.get('token')
|
token: str or None = request.headers.get('SST')
|
||||||
name: str or None = form.get('name')
|
name: str or None = form.get('name')
|
||||||
files: ImmutableMultiDict[str, FileStorage] = request.files
|
files: ImmutableMultiDict[str, FileStorage] = request.files
|
||||||
|
|
||||||
@ -144,8 +144,7 @@ def upload():
|
|||||||
if not name:
|
if not name:
|
||||||
raise KeyError('name')
|
raise KeyError('name')
|
||||||
|
|
||||||
data: dict = {'token': token}
|
user: UserRecord = authenticate_request(token)
|
||||||
user: UserRecord = authenticate_request(data)
|
|
||||||
user_id: str = user.uid
|
user_id: str = user.uid
|
||||||
|
|
||||||
for _, file in files.items():
|
for _, file in files.items():
|
||||||
@ -188,11 +187,15 @@ app.register_blueprint(apiBP)
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description="Background Scheduler")
|
parser = argparse.ArgumentParser(description="Background Scheduler")
|
||||||
parser.add_argument('--interval', type=int, default=10, help="Interval in minutes")
|
(parser.add_argument
|
||||||
|
('--servers-interval', type=int, default=10, help="Interval to check for idle servers. (minutes)"))
|
||||||
|
(parser.add_argument
|
||||||
|
('--modpacks-interval', type=int, default=48, help="Interval to check for modpack updates. (hours)"))
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
scheduler = BackgroundScheduler()
|
scheduler = BackgroundScheduler()
|
||||||
scheduler.add_job(generic_executor.scheduled_actions, 'interval', minutes=args.interval)
|
scheduler.add_job(generic_executor.scheduled_actions_short, 'interval', minutes=args.servers_interval)
|
||||||
|
scheduler.add_job(generic_executor.scheduled_actions_long, 'interval', hours=args.modpacks_interval)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
run_simple('0.0.0.0', 3000, app, use_debugger=False, use_reloader=False)
|
run_simple('0.0.0.0', 3000, app, use_debugger=False, use_reloader=False)
|
||||||
|
@ -22,13 +22,16 @@ def get_user_from_id(user_id):
|
|||||||
|
|
||||||
def verify_jwt_token(token):
|
def verify_jwt_token(token):
|
||||||
try:
|
try:
|
||||||
decoded_token = jwt.decode(token, options={"verify_signature": False})
|
decoded_token = auth.verify_id_token(token, app=app, check_revoked=True)
|
||||||
user_id = decoded_token.get('sub')
|
user_id = decoded_token.get('sub')
|
||||||
return True, user_id
|
return True, user_id
|
||||||
except jwt.ExpiredSignatureError:
|
except jwt.ExpiredSignatureError:
|
||||||
return False, None
|
return False, None
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
return False, None
|
return False, None
|
||||||
|
except Exception as e:
|
||||||
|
log_exception_to_firestore(e, None, {"user": None, "error-step": "auth"})
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
def fetch_port() -> Union[int, None]:
|
def fetch_port() -> Union[int, None]:
|
||||||
@ -130,7 +133,7 @@ def create_server(user_id: str, server_name: str, version: str, port: int, frame
|
|||||||
"pvp": "true",
|
"pvp": "true",
|
||||||
"onlineMode": "true",
|
"onlineMode": "true",
|
||||||
"maxPlayers": "20",
|
"maxPlayers": "20",
|
||||||
"enableCommandBlock": "false"})
|
"enableCommandBlock": "true"})
|
||||||
|
|
||||||
|
|
||||||
def get_server_field(user_id: str, name: str, field_name: str) -> Union[str, bool, None]:
|
def get_server_field(user_id: str, name: str, field_name: str) -> Union[str, bool, None]:
|
||||||
@ -183,7 +186,8 @@ def set_servers_not_running():
|
|||||||
|
|
||||||
for server_doc in server_docs:
|
for server_doc in server_docs:
|
||||||
server_id = server_doc.id
|
server_id = server_doc.id
|
||||||
firestore_database.collection(u'users').document(user_id).collection(u'servers').document(server_id).update({u'running': False})
|
(firestore_database.collection(u'users')
|
||||||
|
.document(user_id).collection(u'servers').document(server_id).update({u'running': False}))
|
||||||
|
|
||||||
print("All servers have been set to not running.")
|
print("All servers have been set to not running.")
|
||||||
|
|
||||||
|
@ -328,9 +328,13 @@ def run_command(user: UserRecord, command: str, name: str) -> tuple[HTTPStatus,
|
|||||||
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Error executing command: {command} || {str(e)}"
|
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Error executing command: {command} || {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
def scheduled_actions() -> None:
|
def scheduled_actions_short() -> None:
|
||||||
mc_manager.check_servers_idle()
|
mc_manager.check_servers_idle()
|
||||||
|
|
||||||
|
|
||||||
|
def scheduled_actions_long() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pass
|
pass
|
||||||
|
@ -9,7 +9,7 @@ rcon.port=25575
|
|||||||
gamemode=survival
|
gamemode=survival
|
||||||
server-port=7878
|
server-port=7878
|
||||||
allow-nether=true
|
allow-nether=true
|
||||||
enable-command-block=false
|
enable-command-block=true
|
||||||
enable-rcon=false
|
enable-rcon=false
|
||||||
sync-chunk-writes=true
|
sync-chunk-writes=true
|
||||||
enable-query=false
|
enable-query=false
|
||||||
|
@ -2,6 +2,9 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
from typing import Callable, Union
|
from typing import Callable, Union
|
||||||
|
|
||||||
|
from firebase_admin import auth
|
||||||
|
from jwt.api_jws import decode_complete
|
||||||
|
|
||||||
import firebase_manager
|
import firebase_manager
|
||||||
import server_mc_manager
|
import server_mc_manager
|
||||||
from generic_executor import mc_manager
|
from generic_executor import mc_manager
|
||||||
@ -41,4 +44,7 @@ if __name__ == '__main__':
|
|||||||
listdir("/home/hapso/Desktop/Personal/servii-backend/servers/paper"),
|
listdir("/home/hapso/Desktop/Personal/servii-backend/servers/paper"),
|
||||||
"/home/hapso/Desktop/Personal/servii-backend/servers/paper")
|
"/home/hapso/Desktop/Personal/servii-backend/servers/paper")
|
||||||
'''
|
'''
|
||||||
|
token: str = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImUwM2E2ODg3YWU3ZjNkMTAyNzNjNjRiMDU3ZTY1MzE1MWUyOTBiNzIiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiSXR6IFNlbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NLOElVZHlzcW5kZkxxNFc5ZWlRNlpjTFpkbUVDX29UNXBVaURGQ2gzY2VDZTZXSGxvWD1zOTYtYyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9zZXJ2aS1lNjcwNSIsImF1ZCI6InNlcnZpLWU2NzA1IiwiYXV0aF90aW1lIjoxNzI2ODI1ODEzLCJ1c2VyX2lkIjoiTXBrYkRNT084UFFkZFFnQjVWZ0JRZFRNV0Y1MyIsInN1YiI6Ik1wa2JETU9POFBRZGRRZ0I1VmdCUWRUTVdGNTMiLCJpYXQiOjE3MjY4MjU4MTMsImV4cCI6MTcyNjgyOTQxMywiZW1haWwiOiJ0ZWNobm9wcm9kMjU0NTg1NjVAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyIxMTQ0Mzk0NjEyOTM5OTE1NzU5MTgiXSwiZW1haWwiOlsidGVjaG5vcHJvZDI1NDU4NTY1QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6Imdvb2dsZS5jb20ifX0.K1xpVX3S83b8AIUShE33bcTcN0XaxA1Uh4oe-sjVE51BGbrpHWw5SNhMzBAiaadPf_mET6-85WdmTYTFcXoDhiC5YhXDfu4fsyQq3K-zwi0ZDNOB0A3Xa7kdsCTwYSxb1DAq3zUZSLH6OHq6af1mGFfsH1WmQ9FT34ULgiBV4W1IHH4PtuYIc1kszgNAxU2lJehi2YsCYB2OZ47VohtOpfYtisJzA9er-L9WmtrMKokxTuCXAuhKIZwb0xAr_ZkZSDx8J1uhGPnPPMeID-7cXXg_tcvCv_WSlTXioQ20hG8J4Lq8Xz1ldQmbcdXl_owqty5m3MdIDiDvP8C9Oc_yLg"
|
||||||
|
decoded = auth.verify_id_token(token, app=firebase_manager.app, check_revoked=True)
|
||||||
|
print(decoded)
|
||||||
pass
|
pass
|
||||||
|
Loading…
Reference in New Issue
Block a user