mirror of
https://github.com/Encryptshell/Pritunl-API-crack.git
synced 2025-11-29 00:23:09 +00:00
Merge branch 'simonmicro:master' into patch-1
This commit is contained in:
@@ -26,12 +26,11 @@ Make sure to support the developers by buying the choosen subscription for your
|
||||
This is _optional_. You can simply use the default instance of this API (host is noted inside the `setup.py` script) and profit from "automatic" updates.
|
||||
|
||||
Just transfer the `www` files inside a public accessible root-folder on your _dedicated_ Apache webserver (really everthing with PHP support works). Also make sure your instance has a valid SSL-certificate (Let's encrypt is enough), otherwise it may won't work.
|
||||
An example Apache install process can be found [here](docs/apache/install.md). If you want to test your instance, just open the public accessible URI in your browser and append `/notification` to it - if you see some JSON with the text, then everything worked!
|
||||
An example Apache install process can be found [here](docs/apache/install.md). If you want to test your instance, just open the public accessible URI in your browser and append `/healthz` to it - if you see some JSON with the text, then everything worked!
|
||||
|
||||
### Nett2Know ###
|
||||
* This modification will also block any communication to the Pritunl servers - so no calling home :)
|
||||
* The `ultimate` mode is still a little bit buggy. This is caused by some hacky workarounds to get all features displayed (the server is already unlocked). Caused by this workaround some items are maybe shown instead of being hidden. If you find such thing - just ping me about it.
|
||||
* SSO will not work with this api version! As Pritunls own authentication servers handle the whole SSO stuff, track instance ids and verify users I won't implement this part for privacy concerns (and also this would need to be securly implemented and need a database).
|
||||
* SSO will not work with this api version! As Pritunls own authentication servers handle the whole SSO stuff, track instance ids and verify users, I won't implement this part for privacy concerns (and also this would need to be securly implemented and a database).
|
||||
* This api has also its own docker image. Take a look into the `docker` folder and enjoy!
|
||||
|
||||
Have fun with your new premium/enterprise/ultimate Pritunl instance!
|
||||
|
||||
1
docker/.dockerignore
Normal file
1
docker/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
mongodb
|
||||
1
docker/.gitignore
vendored
1
docker/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
setup.py
|
||||
mongodb
|
||||
@@ -3,6 +3,4 @@ FROM goofball222/pritunl:latest
|
||||
# Yes, you will need to copy it over into the build context...
|
||||
COPY setup.py .
|
||||
|
||||
RUN chmod +x setup.py
|
||||
RUN python3 -u setup.py --install
|
||||
#RUN rm setup.py
|
||||
RUN chmod +x setup.py; python3 -u setup.py --install; rm setup.py
|
||||
@@ -7,9 +7,10 @@ services:
|
||||
- ./mongodb:/data/db
|
||||
|
||||
pritunl:
|
||||
# Use the following to build the image from source.
|
||||
# Use the following to build the image from source (assuming you're running inside the repository).
|
||||
build:
|
||||
context: .
|
||||
context: ../server
|
||||
dockerfile: ../docker/Dockerfile
|
||||
restart: always
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d43025688bf8e6ddfa1234615e538005af6032c4fe5c3d72b6902eefaa035173
|
||||
size 5095831
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/lib/python3
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import glob
|
||||
import time
|
||||
|
||||
50
www/custom.css
Normal file
50
www/custom.css
Normal file
@@ -0,0 +1,50 @@
|
||||
* {
|
||||
color: rgb(57, 83, 120);
|
||||
}
|
||||
|
||||
.dark * {
|
||||
color: rgb(220, 232, 232);
|
||||
}
|
||||
|
||||
.navbar .navbar-brand {
|
||||
animation-name: pritunl-logo;
|
||||
animation-duration: 20s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes pritunl-logo {
|
||||
0% {
|
||||
transform: rotate3d(1, 0, 0, 360deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate3d(1, 0, 0, 0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate3d(0, 1, 0, 0deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate3d(0, 1, 0, 360deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate3d(0, 1, 0, 360deg);
|
||||
}
|
||||
}
|
||||
|
||||
body::before {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
content: '';
|
||||
background: url("BACKGROUND_IMAGE_URI");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
margin: 1em;
|
||||
opacity: 0.1;
|
||||
z-index: -99;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +0,0 @@
|
||||
/* Enable the display of the uids */
|
||||
.users-list .users-list-title .org-id, .users-list .user .name-container .user-id, .servers-list .server .server-title .server-id, .servers-list .server .org .org-id, .servers-list .server .host .host-id, .servers-list .server .link .link-id {
|
||||
display: inline-block;
|
||||
}
|
||||
3617
www/enterprise_plus.css
Normal file
3617
www/enterprise_plus.css
Normal file
File diff suppressed because one or more lines are too long
265
www/index.php
265
www/index.php
@@ -1,123 +1,164 @@
|
||||
<?php
|
||||
//Author: simonmicro 2022
|
||||
// Author: simonmicro 2023
|
||||
|
||||
header("Access-Control-Allow-Origin: *"); //Allow access from everywhere...
|
||||
$code = 200;
|
||||
// Config
|
||||
$minVersionNumber = 1003235044068;
|
||||
$minVersionName = '1.32.3504.68';
|
||||
$licenseCosts = 42; // insert here any price you want - "0" is a special value, which also breaks the UI ;)
|
||||
|
||||
//Parse body (if possible)
|
||||
header('Access-Control-Allow-Origin: *'); //Allow access from everywhere...
|
||||
$code = 200; // Assuming everything is fine for now
|
||||
|
||||
// Parse body (if possible)
|
||||
$body = json_decode(file_get_contents('php://input'));
|
||||
$clientVersion = isset($body->version) ? $body->version : null;
|
||||
|
||||
//Fake API
|
||||
// Fake API
|
||||
$result = null;
|
||||
if(isset($_GET['path'])) {
|
||||
//Any notification/[version] will be answered here
|
||||
if(preg_match('/notification.*/', $_GET['path'])) {
|
||||
$result = new stdClass;
|
||||
$result->message = 'Fake API endpoint for v1.30.3116.68 active and reachable (contacted at ' . date('r') . ').';
|
||||
$result->vpn = false; //Idk
|
||||
$result->www = false; //Idk
|
||||
} else if(isset($body->license) && preg_match('/subscription.*/', $_GET['path'])) {
|
||||
//The following only works with the body containing the desired license
|
||||
$result = new stdClass;
|
||||
$license = null;
|
||||
//The stylesheet determines what is shown on the dashboard (and by the plan). As default we change the colors of any text.
|
||||
$stylesheet = '';
|
||||
if(preg_match('/.*premium/', $body->license)) {
|
||||
$license = 'premium';
|
||||
} else if(preg_match('/.*enterprise/', $body->license)) {
|
||||
$license = 'enterprise';
|
||||
$stylesheet .= file_get_contents('enterprise.css');
|
||||
//Now fix some too aggressive display strategies by appending their overrides...
|
||||
$stylesheet .= file_get_contents('enterprise_fix.css');
|
||||
} else if(preg_match('/.*ultimate/', $body->license)) {
|
||||
$license = 'enterprise_plus';
|
||||
//Load the new css file and change all invisible blocks to visible (this will show a little bit too much, but whatever...)
|
||||
$stylesheet .= file_get_contents('enterprise.css');
|
||||
$stylesheet = preg_replace('/(enterprise)/', '$1-temp-prefix', $stylesheet);
|
||||
$stylesheet = preg_replace('/(enterprise)(-temp-prefix-plus)/', '$1', $stylesheet);
|
||||
$stylesheet = preg_replace('/(enterprise)(-temp-prefix)/', '$1-plus', $stylesheet);
|
||||
$stylesheet = preg_replace('/(display:.?)none.?$/m', '$1inline-block', $stylesheet); //This WILL SHOW TOO MUCH... So we'll need a fix file...
|
||||
$stylesheet .= file_get_contents('ultimate_fix.css');
|
||||
$path = trim($_GET['path'], ' /');
|
||||
$pathParts = explode('/', $_GET['path']);
|
||||
if(count($pathParts) > 0 && $pathParts[0] == 'healthz') {
|
||||
$result = 'OK';
|
||||
} else if(count($pathParts) > 1 && $pathParts[0] == 'notification') {
|
||||
// Any notification/[version] will be answered here
|
||||
$msg = 'Fake API endpoint for v' . $minVersionName . ' active and reachable (contacted at ' . date('r') . ').';
|
||||
if(intval($pathParts[1]) < $minVersionNumber) {
|
||||
$msg .= ' Please update your Pritunl instance to a newer version as this endpoint may not compatible anymore.';
|
||||
}
|
||||
$stylesheet .= "* { color: rgb(57, 83, 120); }\n.dark * { color: rgb(200, 242, 242); }\n.navbar .navbar-brand { animation-name: pritunl-logo; animation-duration: 20s; animation-iteration-count: infinite; }\n@keyframes pritunl-logo { 0% { transform:rotate3d(1, 0, 0, 360deg); } 25% { transform:rotate3d(1, 0, 0, 0deg); } 50% { transform:rotate3d(0, 1, 0, 0deg); } 75% { transform:rotate3d(0, 1, 0, 360deg); } 100% { transform:rotate3d(0, 1, 0, 360deg); } }\n.footer-brand {visibility: hidden; }\n.footer-brand::before { visibility: visible; position: absolute; bottom: 0; right: 0; content: ''; background: url('https://" . $_SERVER['HTTP_HOST'] . "/logo.png'); background-size: cover; width: 1em; height: 1em; margin: 0.3em; }\n/* Generated for $license license */";
|
||||
$result = array(
|
||||
'message' => $msg,
|
||||
'vpn' => false, // idk
|
||||
'www' => false // idk
|
||||
);
|
||||
} else if(count($pathParts) > 0 && $pathParts[0] == 'auth') {
|
||||
$result = array('error_msg' => 'Sorry, but SSO is currently not supported.');
|
||||
$code = 401; // Let Pritunl fail, without 500 codes (it will show 405)
|
||||
} else if(count($pathParts) > 0 && $pathParts[0] == 'ykwyhd') {
|
||||
// The "you-know-what-you-have-done" endpoint -> used as dummy url target
|
||||
$result = array('detail' => 'You know what you have done.');
|
||||
} else if($clientVersion != null && $clientVersion < $minVersionNumber) {
|
||||
// Check if the instance is too old for us (for now following operators)
|
||||
$result = array('error_msg' => 'This API supports v' . $minVersionName . ' (' . $minVersionNumber . ') or higher.');
|
||||
$code = 473;
|
||||
} else if(count($pathParts) > 0 && $pathParts[0] == 'subscription') {
|
||||
// The following only works with the body containing the desired license
|
||||
if(isset($body->license)) {
|
||||
$license = null;
|
||||
$user = md5(base64_encode($body->license));
|
||||
$url_key = substr($user, 0, 8);
|
||||
$input = strtolower($body->license);
|
||||
|
||||
# Workaround for 70b354a10df55d60515f76d851dee42939864395
|
||||
if($body->version >= 1003031084050 and $body->version < 1003031164068)
|
||||
$stylesheet = base64_encode($stylesheet);
|
||||
|
||||
$state = null;
|
||||
if($license) { //The following only makes sense if you selected any license
|
||||
if(strpos($body->license, 'bad') !== false) {
|
||||
$state = 'Bad';
|
||||
} else if(strpos($body->license, 'canceled') !== false) {
|
||||
$state = 'canceled';
|
||||
} else if(strpos($body->license, 'active') !== false) {
|
||||
$state = 'Active';
|
||||
// The stylesheet determines what is shown on the dashboard (and by the plan).
|
||||
$stylesheet = '';
|
||||
if(str_contains($input, 'premium')) {
|
||||
$license = 'premium';
|
||||
$stylesheet = file_get_contents('premium.css');
|
||||
// No need to install the user license "id" into CSS class, as that file only contains custom patches
|
||||
} else if(str_contains($input, 'enterprise')) {
|
||||
$license = 'enterprise';
|
||||
$stylesheet = file_get_contents('enterprise.css');
|
||||
$stylesheet = preg_replace('/(\.enterprise)([\.\ ])/', '$1-'.$url_key.'$2', $stylesheet); // Install user license "id" into CSS class
|
||||
} else if(str_contains($input, 'ultimate')) {
|
||||
$license = 'enterprise_plus';
|
||||
$stylesheet = file_get_contents('enterprise_plus.css');
|
||||
$stylesheet = preg_replace('/(\.enterprise-plus)([\.\ ])/', '$1-'.$url_key.'$2', $stylesheet); // Install user license "id" into CSS class
|
||||
}
|
||||
}
|
||||
$stylesheet .= "\n/* custom.css */\n";
|
||||
$stylesheet .= str_replace('BACKGROUND_IMAGE_URI', "https://" . $_SERVER['HTTP_HOST'] . "/logo.png", file_get_contents('custom.css'));
|
||||
$stylesheet .= "\n/* Generated for $license license */";
|
||||
|
||||
if($state == 'Active') {
|
||||
$result->active = $body->version < 1003031164068 ? $license != 'premium' : $license == 'enterprise_plus';
|
||||
$result->status = $state;
|
||||
$result->plan = $license;
|
||||
$result->quantity = 42;
|
||||
$result->amount = 42;
|
||||
$result->credit = 42;
|
||||
$result->period_end = false;
|
||||
$result->trial_end = false;
|
||||
$result->cancel_at_period_end = false;
|
||||
$result->styles = new stdClass;
|
||||
$result->styles->etag = 42;
|
||||
$result->styles->last_modified = time();
|
||||
$result->styles->data = $stylesheet;
|
||||
$state = null;
|
||||
if($license) { // The following only makes sense if you selected any license
|
||||
if(str_starts_with($input, 'bad')) {
|
||||
$state = 'Bad';
|
||||
} else if(str_starts_with($input, 'canceled')) {
|
||||
$state = 'canceled';
|
||||
} else if(str_starts_with($input, 'active')) {
|
||||
$state = 'Active';
|
||||
}
|
||||
}
|
||||
|
||||
if($state == 'Active') {
|
||||
$result = array(
|
||||
'active' => true, // if the sub is not active, the css won't use the LICENSE-subscription_id pattern
|
||||
'status' => $state,
|
||||
'plan' => $license,
|
||||
'url_key' => $user,
|
||||
'quantity' => 42,
|
||||
'amount' => $licenseCosts,
|
||||
'credit' => 42,
|
||||
'period_end' => false,
|
||||
'trial_end' => false,
|
||||
'cancel_at_period_end' => false,
|
||||
'premium_buy_url' => 'https://' . $_SERVER['HTTP_HOST'] . '/ykwyhd/',
|
||||
'enterprise_buy_url' => 'https://' . $_SERVER['HTTP_HOST'] . '/ykwyhd/',
|
||||
'portal_url' => 'https://' . $_SERVER['HTTP_HOST'] . '/ykwyhd/',
|
||||
'styles' => array(
|
||||
'etag' => null, // the resource is NOT encrypted
|
||||
'last_modified' => time(),
|
||||
'data' => $stylesheet
|
||||
)
|
||||
);
|
||||
} else if($state == 'Canceled') {
|
||||
$result = array(
|
||||
'active' => false, // Here we can savely disable any style
|
||||
'status' => $state,
|
||||
'plan' => $license,
|
||||
'quantity' => 42,
|
||||
'amount' => 42,
|
||||
'period_end' => false,
|
||||
'trial_end' => false,
|
||||
'cancel_at_period_end' => false,
|
||||
'styles' => array(
|
||||
'etag' => null,
|
||||
'last_modified' => null,
|
||||
'data' => null
|
||||
)
|
||||
);
|
||||
} else if($state == 'Bad' || $state == null) {
|
||||
$code = 470; // -> bad license
|
||||
// Do not mention "canceled" in "error_msg", as it is somewhat useless (same as bad)...
|
||||
$result = array(
|
||||
'error' => 'license_invalid',
|
||||
'error_msg' => $state == null ? 'Unknown command. Use ["bad" | "active"] ["premium" | "enterprise" | "ultimate"].' : 'As you wish.',
|
||||
'active' => false,
|
||||
'status' => null,
|
||||
'plan' => null,
|
||||
'quantity' => null,
|
||||
'amount' => null,
|
||||
'period_end' => null,
|
||||
'trial_end' => null,
|
||||
'cancel_at_period_end' => null,
|
||||
'styles' => array(
|
||||
'etag' => null,
|
||||
'last_modified' => null,
|
||||
'data' => null
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$result = array('error_msg' => 'Missing license in body.');
|
||||
$code = 401;
|
||||
}
|
||||
if($state == 'Canceled') {
|
||||
$result->active = false; //Here we can savely disable any styles
|
||||
$result->status = $state;
|
||||
$result->plan = $license;
|
||||
$result->quantity = 42;
|
||||
$result->amount = 42;
|
||||
$result->period_end = false;
|
||||
$result->trial_end = false;
|
||||
$result->cancel_at_period_end = false;
|
||||
$result->styles = new stdClass;
|
||||
$result->styles->etag = 42;
|
||||
$result->styles->last_modified = time();
|
||||
$result->styles->data = $stylesheet;
|
||||
}
|
||||
if($state == 'Bad' || $state == null) {
|
||||
$code = 470; //-> bad license
|
||||
$result->error_msg = 'As you wish.';
|
||||
$result->error = 'license_invalid';
|
||||
$result->active = false;
|
||||
$result->status = false;
|
||||
$result->plan = null;
|
||||
$result->quantity = 0;
|
||||
$result->amount = 0;
|
||||
$result->period_end = true;
|
||||
$result->trial_end = true;
|
||||
$result->cancel_at_period_end = null;
|
||||
$result->styles = new stdClass;
|
||||
}
|
||||
if($state == null) {
|
||||
$result->error_msg = 'Unknown command. Use ["bad" | "canceled" | "active"] ["premium" | "enterprise" | "ultimate"].';
|
||||
}
|
||||
} else if(preg_match('/checkout.*/', $_GET['path'])) {
|
||||
$result = array();
|
||||
$result['zipCode'] = false;
|
||||
$result['allowRememberMe'] = false;
|
||||
$result['image'] = 'https://' . $_SERVER['HTTP_HOST'] . '/logo.png';
|
||||
$result['key'] = null; //Insert here a key to unlock the stripe store (is a string). And buy the subscription...
|
||||
$result['plans'] = array();
|
||||
$result['plans']['premium'] = array();
|
||||
$result['plans']['premium']['amount'] = 42;
|
||||
$result['plans']['enterprise'] = array();
|
||||
$result['plans']['enterprise']['amount'] = 42;
|
||||
$result['plans']['enterprise_plus'] = array();
|
||||
$result['plans']['enterprise_plus']['amount'] = 42;
|
||||
} else if(preg_match('/auth\/.*/', $_GET['path'])) {
|
||||
$result = array('error' => 'Sorry, but SSO is currently not supported.');
|
||||
$code = 401; //Let Pritunl fail, without 500 codes (it will show 405)
|
||||
} else if(count($pathParts) > 0 && $pathParts[0] == 'checkout') {
|
||||
$result = array(
|
||||
'zipCode' => false,
|
||||
'allowRememberMe' => false,
|
||||
'image' => 'https://' . $_SERVER['HTTP_HOST'] . '/logo.png',
|
||||
'key' => null, // Insert here a key to unlock the stripe store (is a string). And buy the subscription...
|
||||
'plans' => array(
|
||||
'premium' => array(
|
||||
'amount' => $licenseCosts
|
||||
),
|
||||
'enterprise' => array(
|
||||
'amount' => $licenseCosts
|
||||
),
|
||||
'enterprise_plus' => array(
|
||||
'amount' => $licenseCosts
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,12 +166,12 @@ header('Content-Type: application/json');
|
||||
http_response_code($code);
|
||||
echo json_encode($result);
|
||||
|
||||
//Should we log any request? Used for the development and debugging of this API
|
||||
// Should we log any request? Used for the development and debugging of this API
|
||||
if(false) {
|
||||
//Log request
|
||||
file_put_contents('access.log', "\n" . date('r') . ":\t" . json_encode(array('head' => getallheaders(), 'body' => file_get_contents('php://input'), 'get' => $_GET, 'post' => $_POST, 'answer_code' => $code, 'answer' => $result)) . "\n", FILE_APPEND);
|
||||
// Log request
|
||||
file_put_contents('access.log', "\n" . date('r') . ":\n" . json_encode(array('head' => getallheaders(), 'body' => file_get_contents('php://input'), 'get' => $_GET, 'post' => $_POST, 'answer_code' => $code, 'answer' => $result)) . "\n", FILE_APPEND);
|
||||
|
||||
//GET operator to clear log file
|
||||
// GET operator to clear log file
|
||||
if(isset($_GET['clear']))
|
||||
file_put_contents('access.log', '');
|
||||
}
|
||||
|
||||
12
www/premium.css
Normal file
12
www/premium.css
Normal file
@@ -0,0 +1,12 @@
|
||||
/* Fixes for the premium subscription-modal, which seems to be empty / broken in recent versions */
|
||||
.enterprise-modal .enterprise-info {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.enterprise-modal .modal .enterprise-info .premium-plan {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.enterprise-modal .enterprise-buttons {
|
||||
display: inherit;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/* Fix the sub info */
|
||||
.enterprise-modal .enterprise-info {
|
||||
display: block;
|
||||
}
|
||||
.enterprise-modal .enterprise-buttons {
|
||||
display: block;
|
||||
}
|
||||
/* And show the enterprise+ (our ultimate) plan */
|
||||
.enterprise-modal .modal .enterprise-info .enterprise-plus-plan {
|
||||
display: block;
|
||||
}
|
||||
/* @Servers Fix bad spacing? */
|
||||
.servers-list .server .server-info .server-output-link-viewer {
|
||||
display: none;
|
||||
}
|
||||
.servers-list .server .server-info .server-graph-viewer {
|
||||
display: none;
|
||||
}
|
||||
/* @Servers Fix double shown IPs */
|
||||
.enterprise-plus .server-routes-list .route .route-network-no-click {
|
||||
display: none;
|
||||
}
|
||||
/* Fix wrongly shown NAT flag */
|
||||
.enterprise-plus .server-routes-list .route .route-nat {
|
||||
display: none;
|
||||
}
|
||||
.server-routes-list .route .route-nat {
|
||||
display: none;
|
||||
}
|
||||
.server-routes-list .route .route-nat, .server-routes-list .route .route-nat-netmap, .server-routes-list .route .route-net-gateway, .server-routes-list .route .route-default, .server-routes-list .route .route-virtual-network, .server-routes-list .route .route-network-link, .server-routes-list .route .route-server-link, .server-routes-list .route .route-vpc-id {
|
||||
display: none;
|
||||
}
|
||||
/* Fix selectors */
|
||||
.selector .selector-inner {
|
||||
display: none;
|
||||
}
|
||||
/* Remove second useless Theme selector in settings */
|
||||
.enterprise-plus .settings-modal .right .form-group.theme {
|
||||
display: none;
|
||||
}
|
||||
/* Hide advanced settings... So the button is useable again... */
|
||||
.modal .modal-body .advanced {
|
||||
display: none;
|
||||
}
|
||||
/* Hide admins tab, it will be shown only if the user is a super-user */
|
||||
.nav .admins {
|
||||
display: none;
|
||||
}
|
||||
Reference in New Issue
Block a user