Merge remote-tracking branch 'upstream/develop' into gobgp_integration
This commit is contained in:
commit
166a3f0fcf
3
.gitignore
vendored
3
.gitignore
vendored
@ -28,9 +28,8 @@ DIST/
|
||||
|
||||
var/
|
||||
|
||||
etc/alicelg/alice.conf
|
||||
etc/alice-lg/alice.conf
|
||||
|
||||
.DS_Store
|
||||
|
||||
*coverage*
|
||||
|
||||
|
18
Makefile
18
Makefile
@ -10,7 +10,7 @@ ARCH=amd64
|
||||
|
||||
SYSTEM_INIT=systemd
|
||||
|
||||
# == END BUILD CONFIGURATION ==
|
||||
# == END BUILD CONFIGURATION ==
|
||||
|
||||
VERSION=$(shell cat ./VERSION)
|
||||
|
||||
@ -45,16 +45,16 @@ dev:
|
||||
backend_prod: client_prod
|
||||
$(MAKE) -C backend/ bundle
|
||||
$(MAKE) -C backend/ linux
|
||||
|
||||
|
||||
|
||||
alice: client_prod backend_prod
|
||||
mv backend/alice-lg-* bin/
|
||||
|
||||
|
||||
dist: clean alice
|
||||
dist: clean alice
|
||||
|
||||
mkdir -p $(DIST)opt/ecix/alicelg/bin
|
||||
mkdir -p $(DIST)etc/alicelg
|
||||
mkdir -p $(DIST)opt/alice-lg/alice-lg/bin
|
||||
mkdir -p $(DIST)etc/alice-lg
|
||||
|
||||
# Adding post install script
|
||||
cp install/scripts/after_install $(DIST)/.
|
||||
@ -70,10 +70,10 @@ else
|
||||
endif
|
||||
|
||||
# Copy example configuration
|
||||
cp etc/alicelg/alice.example.conf $(DIST)/etc/alicelg/alice.example.conf
|
||||
cp etc/alice-lg/alice.example.conf $(DIST)/etc/alice-lg/alice.example.conf
|
||||
|
||||
# Copy application
|
||||
cp bin/$(PROG)-linux-$(ARCH) DIST/opt/ecix/alicelg/bin/.
|
||||
cp bin/$(PROG)-linux-$(ARCH) DIST/opt/ecix/alice-lg/bin/.
|
||||
|
||||
|
||||
rpm: dist
|
||||
@ -84,7 +84,7 @@ rpm: dist
|
||||
# Create RPM from dist
|
||||
fpm -s dir -t rpm -n $(PROG) -v $(VERSION) -C $(DIST) \
|
||||
--architecture $(ARCH) \
|
||||
--config-files /etc/alicelg/alice.example.conf \
|
||||
--config-files /etc/alice-lg/alice.example.conf \
|
||||
--after-install $(DIST)/after_install \
|
||||
opt/ etc/
|
||||
|
||||
@ -105,7 +105,7 @@ remote_rpm: build_server dist
|
||||
scp -r $(DIST) $(BUILD_SERVER):$(REMOTE_DIST)
|
||||
ssh $(BUILD_SERVER) -- fpm -s dir -t rpm -n $(PROG) -v $(VERSION) -C $(REMOTE_DIST) \
|
||||
--architecture $(ARCH) \
|
||||
--config-files /etc/alicelg/alice.example.conf \
|
||||
--config-files /etc/alice-lg/alice.example.conf \
|
||||
--after-install $(REMOTE_DIST)/after_install \
|
||||
opt/ etc/
|
||||
|
||||
|
26
README.md
26
README.md
@ -3,7 +3,7 @@ __"No, no! The adventures first, explanations take such a dreadful time."__
|
||||
_Lewis Carroll, Alice's Adventures in Wonderland & Through the Looking-Glass_
|
||||
|
||||
Take a look at an Alice-LG production examples at:
|
||||
- https://lg-beta.de-cix.net/
|
||||
- https://lg.de-cix.net/
|
||||
- https://lg.ecix.net/
|
||||
|
||||
And checkout the API at:
|
||||
@ -23,7 +23,7 @@ Currently Alice-LG supports the following APIs:
|
||||
Normally you would first install the [birdwatcher API](https://github.com/ecix/birdwatcher) directly on the machine(s) where you run [BIRD](http://bird.network.cz/) on
|
||||
and then install Alice-LG on a seperate public facing server and point her to the afore mentioned [birdwatcher API](https://github.com/ecix/birdwatcher).
|
||||
|
||||
This project was a direct result of the [RIPE IXP Tools Hackathon](https://atlas.ripe.net/hackathon/ixp-tools/)
|
||||
This project was a direct result of the [RIPE IXP Tools Hackathon](https://atlas.ripe.net/hackathon/ixp-tools/)
|
||||
just prior to [RIPE73](https://ripe73.ripe.net/) in Madrid, Spain.
|
||||
|
||||
Major thanks to Barry O'Donovan who built the original [INEX Bird's Eye](https://github.com/inex/birdseye) BIRD API of which Alice-LG is a spinnoff
|
||||
@ -63,17 +63,17 @@ Your Alice-LG source will now be located at `~/go/src/github.com/alice-lg/alice-
|
||||
|
||||
## Configuration
|
||||
|
||||
An example configuration can be found at
|
||||
[etc/alicelg/alice.example.conf](https://github.com/ecix/alice-lg/blob/readme_update/etc/alicelg/alice.example.conf).
|
||||
An example configuration can be found at
|
||||
[etc/alice-lg/alice.example.conf](https://github.com/alice-lg/alice-lg/blob/readme_update/etc/alice-lg/alice.example.conf).
|
||||
|
||||
You can copy it to any of the following locations:
|
||||
|
||||
etc/alicelg/alice.conf # local
|
||||
etc/alicelg/alice.local.conf # local
|
||||
/etc/alicelg/alice.conf # global
|
||||
etc/alice-lg/alice.conf # local
|
||||
etc/alice-lg/alice.local.conf # local
|
||||
/etc/alice-lg/alice.conf # global
|
||||
|
||||
|
||||
You will have to edit the configuration file as you need to point Alice-LG to the correct [APIs](https://github.com/ecix/birdwatcher):
|
||||
You will have to edit the configuration file as you need to point Alice-LG to the correct [APIs](https://github.com/alice-lg/birdwatcher):
|
||||
|
||||
```ini
|
||||
[source.0]
|
||||
@ -127,15 +127,15 @@ In your alice.conf, you now can specify a theme by setting:
|
||||
|
||||
with the optional parameter (the "mountpoint" of the theme)
|
||||
url_base = /theme
|
||||
|
||||
|
||||
You can put assets (images, fonts, javscript, css) in
|
||||
|
||||
You can put assets (images, fonts, javscript, css) in
|
||||
this folder.
|
||||
|
||||
Stylesheets and Javascripts are automatically included in
|
||||
the client's html and are served from the backend.
|
||||
|
||||
Alice provides early stages of an extension API, which is for now
|
||||
Alice provides early stages of an extension API, which is for now
|
||||
only used to modify the content of the welcome screen,
|
||||
by providing a javascript in your theme containing:
|
||||
|
||||
@ -146,7 +146,7 @@ Alice.updateContent({
|
||||
tagline: "powered by Alice"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
```
|
||||
|
||||
For an example check out: https://github.com/alice-lg/alice-theme-example
|
||||
@ -154,7 +154,7 @@ For an example check out: https://github.com/alice-lg/alice-theme-example
|
||||
## Hacking
|
||||
|
||||
The client is a Single Page React Application.
|
||||
All sources are available in `client/`.
|
||||
All sources are available in `client/`.
|
||||
|
||||
Install build tools as needed:
|
||||
|
||||
|
@ -51,9 +51,9 @@ func endpoint(wrapped apiEndpoint) httprouter.Handle {
|
||||
}
|
||||
|
||||
// Make error response
|
||||
result = apiErrorResponse(rsId, err)
|
||||
result, status := apiErrorResponse(rsId, err)
|
||||
payload, _ := json.Marshal(result)
|
||||
http.Error(res, string(payload), http.StatusInternalServerError)
|
||||
http.Error(res, string(payload), status)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,12 @@ func apiStatus(_req *http.Request, params httprouter.Params) (api.Response, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
source := AliceConfig.SourceById(rsId).getInstance()
|
||||
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.Status()
|
||||
if err != nil {
|
||||
apiLogSourceError("status", rsId, err)
|
||||
|
@ -37,7 +37,11 @@ func apiNeighborsList(_req *http.Request, params httprouter.Params) (api.Respons
|
||||
Neighbours: neighbors,
|
||||
}
|
||||
} else {
|
||||
source := AliceConfig.SourceById(rsId).getInstance()
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
neighborsResponse, err = source.Neighbours()
|
||||
if err != nil {
|
||||
apiLogSourceError("neighbors", rsId, err)
|
||||
|
@ -15,7 +15,12 @@ func apiRoutesList(_req *http.Request, params httprouter.Params) (api.Response,
|
||||
return nil, err
|
||||
}
|
||||
neighborId := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceById(rsId).getInstance()
|
||||
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.Routes(neighborId)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes", rsId, neighborId, err)
|
||||
@ -38,7 +43,11 @@ func apiRoutesListReceived(
|
||||
}
|
||||
|
||||
neighborId := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceById(rsId).getInstance()
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.RoutesReceived(neighborId)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes_received", rsId, neighborId, err)
|
||||
@ -107,7 +116,11 @@ func apiRoutesListFiltered(
|
||||
}
|
||||
|
||||
neighborId := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceById(rsId).getInstance()
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.RoutesFiltered(neighborId)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes_filtered", rsId, neighborId, err)
|
||||
@ -176,7 +189,11 @@ func apiRoutesListNotExported(
|
||||
}
|
||||
|
||||
neighborId := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceById(rsId).getInstance()
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.RoutesNotExported(neighborId)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes_not_exported", rsId, neighborId, err)
|
||||
|
@ -6,30 +6,51 @@ package main
|
||||
// to internal IP addresses.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/alice-lg/alice-lg/backend/api"
|
||||
)
|
||||
|
||||
type ResourceNotFoundError struct{}
|
||||
|
||||
func (self *ResourceNotFoundError) Error() string {
|
||||
return "resource not found"
|
||||
}
|
||||
|
||||
var SOURCE_NOT_FOUND_ERROR = &ResourceNotFoundError{}
|
||||
|
||||
const (
|
||||
GENERIC_ERROR_TAG = "GENERIC_ERROR"
|
||||
CONNECTION_REFUSED_TAG = "CONNECTION_REFUSED"
|
||||
CONNECTION_TIMEOUT_TAG = "CONNECTION_TIMEOUT"
|
||||
RESOURCE_NOT_FOUND_TAG = "NOT_FOUND"
|
||||
)
|
||||
|
||||
const (
|
||||
GENERIC_ERROR_CODE = 42
|
||||
CONNECTION_REFUSED_CODE = 100
|
||||
CONNECTION_TIMEOUT_CODE = 101
|
||||
RESOURCE_NOT_FOUND_CODE = 404
|
||||
)
|
||||
|
||||
func apiErrorResponse(routeserverId string, err error) api.ErrorResponse {
|
||||
const (
|
||||
ERROR_STATUS = http.StatusInternalServerError
|
||||
RESOURCE_NOT_FOUND_STATUS = http.StatusNotFound
|
||||
)
|
||||
|
||||
func apiErrorResponse(routeserverId string, err error) (api.ErrorResponse, int) {
|
||||
code := GENERIC_ERROR_CODE
|
||||
message := err.Error()
|
||||
tag := GENERIC_ERROR_TAG
|
||||
status := ERROR_STATUS
|
||||
|
||||
switch e := err.(type) {
|
||||
case *ResourceNotFoundError:
|
||||
tag = RESOURCE_NOT_FOUND_TAG
|
||||
code = RESOURCE_NOT_FOUND_CODE
|
||||
status = RESOURCE_NOT_FOUND_STATUS
|
||||
case *url.Error:
|
||||
if strings.Contains(message, "connection refused") {
|
||||
tag = CONNECTION_REFUSED_TAG
|
||||
@ -47,5 +68,5 @@ func apiErrorResponse(routeserverId string, err error) api.ErrorResponse {
|
||||
Tag: tag,
|
||||
Message: message,
|
||||
RouteserverId: routeserverId,
|
||||
}
|
||||
}, status
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
// Helper: Validate source Id
|
||||
func validateSourceId(id string) (string, error) {
|
||||
if len(id) > 15 {
|
||||
if len(id) > 42 {
|
||||
return "unknown", fmt.Errorf("Source ID too long with length: %d", len(id))
|
||||
}
|
||||
return id, nil
|
||||
|
@ -115,6 +115,17 @@ func (self *Config) SourceById(sourceId string) *SourceConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get instance by id
|
||||
func (self *Config) SourceInstanceById(sourceId string) sources.Source {
|
||||
sourceConfig := self.SourceById(sourceId)
|
||||
if sourceConfig == nil {
|
||||
return nil // Nothing to do here.
|
||||
}
|
||||
|
||||
// Get instance from config
|
||||
return sourceConfig.getInstance()
|
||||
}
|
||||
|
||||
// Get sources keys form ini
|
||||
func getSourcesKeys(config *ini.File) []string {
|
||||
sources := []string{}
|
||||
@ -624,9 +635,9 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
|
||||
// Try to load configfiles as specified in the files
|
||||
// list. For example:
|
||||
//
|
||||
// ./etc/alicelg/alice.conf
|
||||
// /etc/alicelg/alice.conf
|
||||
// ./etc/alicelg/alice.local.conf
|
||||
// ./etc/alice-lg/alice.conf
|
||||
// /etc/alice-lg/alice.conf
|
||||
// ./etc/alice-lg/alice.local.conf
|
||||
//
|
||||
func loadConfig(file string) (*Config, error) {
|
||||
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
func TestLoadConfigs(t *testing.T) {
|
||||
|
||||
config, err := loadConfig("../etc/alicelg/alice.example.conf")
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
}
|
||||
@ -39,7 +39,7 @@ func TestLoadConfigs(t *testing.T) {
|
||||
|
||||
func TestSourceConfigDefaultsOverride(t *testing.T) {
|
||||
|
||||
config, err := loadConfig("../etc/alicelg/alice.example.conf")
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
}
|
||||
@ -70,7 +70,7 @@ func TestSourceConfigDefaultsOverride(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRejectAndNoexportReasons(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alicelg/alice.example.conf")
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
}
|
||||
@ -97,7 +97,7 @@ func TestRejectAndNoexportReasons(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBlackholeParsing(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alicelg/alice.example.conf")
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
}
|
||||
@ -116,7 +116,7 @@ func TestBlackholeParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOwnASN(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alicelg/alice.example.conf")
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
}
|
||||
@ -127,7 +127,7 @@ func TestOwnASN(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRpkiConfig(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alicelg/alice.example.conf")
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
}
|
||||
@ -157,7 +157,7 @@ func TestRpkiConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRejectCandidatesConfig(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alicelg/alice.example.conf")
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
return
|
||||
|
@ -17,7 +17,7 @@ func main() {
|
||||
|
||||
// Handle commandline parameters
|
||||
configFilenameFlag := flag.String(
|
||||
"config", "/etc/alicelg/alice.conf",
|
||||
"config", "/etc/alice-lg/alice.conf",
|
||||
"Alice looking glass configuration file",
|
||||
)
|
||||
|
||||
|
@ -119,6 +119,10 @@ func parseBirdwatcherStatus(bird ClientResponse, config Config) (api.Status, err
|
||||
config.Timezone,
|
||||
)
|
||||
|
||||
if config.ShowLastReboot == false {
|
||||
lastReboot = time.Time{}
|
||||
}
|
||||
|
||||
lastReconfig, _ := parseServerTime(
|
||||
birdStatus["last_reconfig"],
|
||||
config.ServerTimeExt,
|
||||
|
@ -4,9 +4,9 @@
|
||||
# Use node:10 as base image
|
||||
#
|
||||
|
||||
FROM node:10
|
||||
FROM node:11
|
||||
|
||||
RUN npm install -g gulp
|
||||
RUN npm install -g gulp@4.0.0
|
||||
RUN npm install -g gulp-cli
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ image:
|
||||
docker build . -t $(DOCKER_IMAGE)
|
||||
|
||||
deps: image
|
||||
$(DOCKER_EXEC) "npm install"
|
||||
$(DOCKER_EXEC) "yarn install"
|
||||
|
||||
client: stop deps
|
||||
@echo "Building alice UI"
|
||||
|
@ -175,22 +175,26 @@ class RoutesPage extends React.Component {
|
||||
<RoutesViewEmpty routes={this.props.routes}
|
||||
loadNotExported={this.props.loadNotExported} />
|
||||
|
||||
|
||||
<RoutesView
|
||||
type={ROUTES_FILTERED}
|
||||
routeserverId={this.props.params.routeserverId}
|
||||
protocolId={this.props.params.protocolId} />
|
||||
|
||||
{this.props.receivedLoading && <RoutesLoadingIndicator />}
|
||||
|
||||
<RoutesView
|
||||
type={ROUTES_RECEIVED}
|
||||
routeserverId={this.props.params.routeserverId}
|
||||
protocolId={this.props.params.protocolId} />
|
||||
|
||||
{this.props.notExportedLoading && <RoutesLoadingIndicator />}
|
||||
|
||||
<RoutesView
|
||||
type={ROUTES_NOT_EXPORTED}
|
||||
routeserverId={this.props.params.routeserverId}
|
||||
protocolId={this.props.params.protocolId} />
|
||||
|
||||
<RoutesLoadingIndicator />
|
||||
|
||||
</div>
|
||||
<div className="col-lg-3 col-md-12 col-aside-details">
|
||||
@ -290,7 +294,12 @@ export default connect(
|
||||
filtersApplied: filtersApplied,
|
||||
},
|
||||
|
||||
relatedPeers: relatedPeers
|
||||
relatedPeers: relatedPeers,
|
||||
|
||||
// Loding indicator helper
|
||||
receivedLoading: state.routes.receivedLoading,
|
||||
filteredLoading: state.routes.filteredLoading,
|
||||
notExportedLoading: state.routes.notExportedLoading
|
||||
});
|
||||
}
|
||||
)(RoutesPage);
|
||||
|
7639
client/package-lock.json
generated
7639
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
3082
client/yarn.lock
3082
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -5,12 +5,11 @@
|
||||
# Create the required user and set permissions
|
||||
#
|
||||
|
||||
SERVICE=alicelg
|
||||
SERVICE=alice-lg
|
||||
|
||||
echo "[i] Post install $SERVICE"
|
||||
echo "[i] Creating user and updating permissions"
|
||||
useradd --system -d /opt/ecix/$SERVICE $SERVICE
|
||||
useradd --system -d /opt/alice-lg/$SERVICE $SERVICE
|
||||
|
||||
echo "[i] Fixing permissions"
|
||||
chown -R $SERVICE:$SERVICE /opt/ecix/$SERVICE
|
||||
|
||||
chown -R $SERVICE:$SERVICE /opt/alice-lg/$SERVICE
|
||||
|
@ -5,8 +5,8 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=alicelg
|
||||
ExecStart=/opt/ecix/alicelg/bin/alice-lg-linux-amd64
|
||||
User=alice-lg
|
||||
ExecStart=/opt/alice-lg/alice-lg/bin/alice-lg-linux-amd64
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,5 +1,5 @@
|
||||
|
||||
# Alice Looking Glass
|
||||
# Alice Looking Glass
|
||||
|
||||
description "Alice Looking Glass"
|
||||
author "Matthias Hannig <mha@ecix.net>"
|
||||
@ -10,5 +10,4 @@ respawn limit 20 10
|
||||
start on runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
exec su -l alicelg -c /opt/ecix/alicelg/bin/alice-lg-linux-amd64 2>&1 | logger -i -t 'ALICE LG'
|
||||
|
||||
exec su -l alice-lg -c /opt/alice-lg/alice-lg/bin/alice-lg-linux-amd64 2>&1 | logger -i -t 'ALICE LG'
|
||||
|
Loading…
x
Reference in New Issue
Block a user