Removed GoJi in favor of Pressly Chi
This commit is contained in:
Generated
+5
-13
@@ -1,20 +1,12 @@
|
||||
hash: 0a451841a1f9493d419d658fab8da6863ef5df0645ddea506a225865078c9dcb
|
||||
updated: 2016-08-10T22:23:10.989237578-03:00
|
||||
hash: ee2de935e70f2a39cbfd2dd16add0736c698b32f5c02409d0c25844d883c91a1
|
||||
updated: 2016-08-22T22:55:07.279113421-03:00
|
||||
imports:
|
||||
- name: github.com/golang/glog
|
||||
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
|
||||
- name: github.com/gorilla/websocket
|
||||
version: a69d25be2fe2923a97c2af6849b2f52426f68fc0
|
||||
- name: github.com/pusher/pusher-http-go
|
||||
version: 2bba5f217f6f0f4f0c0a9bb11b945b206b32bec5
|
||||
- name: goji.io
|
||||
version: e355964ac565b94cf0fc7f218346626529125086
|
||||
- name: github.com/pressly/chi
|
||||
version: 12aad88c7d86de2affe686f855b6ed94a07cba9c
|
||||
subpackages:
|
||||
- pat
|
||||
- pattern
|
||||
- internal
|
||||
- name: golang.org/x/net
|
||||
version: 075e191f18186a8ff2becaf64478e30f4545cdad
|
||||
subpackages:
|
||||
- context
|
||||
- middleware
|
||||
testImports: []
|
||||
|
||||
+3
-7
@@ -2,10 +2,6 @@ package: github.com/dimiro1/ipe
|
||||
import:
|
||||
- package: github.com/golang/glog
|
||||
- package: github.com/gorilla/websocket
|
||||
- package: github.com/pusher/pusher-http-go
|
||||
- package: goji.io
|
||||
subpackages:
|
||||
- pat
|
||||
- package: golang.org/x/net
|
||||
subpackages:
|
||||
- context
|
||||
- package: github.com/pressly/chi
|
||||
excludeDirs:
|
||||
- functional
|
||||
+54
-85
@@ -12,12 +12,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
goji "goji.io"
|
||||
|
||||
"goji.io/pat"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"golang.org/x/net/context"
|
||||
"github.com/pressly/chi"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
)
|
||||
@@ -51,79 +47,64 @@ func prepareQueryString(params url.Values) string {
|
||||
// * The request path (e.g. /some/resource)
|
||||
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
|
||||
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
|
||||
func restAuthenticationHandler(DB db, next goji.Handler) goji.HandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
func authenticationHandler(DB db) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
|
||||
app, err := DB.GetAppByAppID(appID)
|
||||
app, err := DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
signature := query.Get("auth_signature")
|
||||
query.Del("auth_signature")
|
||||
|
||||
queryString := prepareQueryString(query)
|
||||
|
||||
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
||||
|
||||
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
log.Error("Not authorized")
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
signature := query.Get("auth_signature")
|
||||
query.Del("auth_signature")
|
||||
|
||||
queryString := prepareQueryString(query)
|
||||
|
||||
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
|
||||
|
||||
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
|
||||
next.ServeHTTPC(ctx, w, r)
|
||||
} else {
|
||||
log.Error("Not authorized")
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the application is disabled
|
||||
func restCheckAppDisabledHandler(DB db, next goji.Handler) goji.HandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
func checkAppDisabled(DB db) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
|
||||
currentApp, err := DB.GetAppByAppID(appID)
|
||||
currentApp, err := DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if currentApp.ApplicationDisabled {
|
||||
http.Error(w, "Application disabled", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTPC(ctx, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func recoverHandler(next goji.Handler) goji.HandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Please verify the url parameters error was: %s", r)
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}()
|
||||
next.ServeHTTPC(ctx, w, r)
|
||||
|
||||
if currentApp.ApplicationDisabled {
|
||||
http.Error(w, "Application disabled", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers
|
||||
func commonHandlers(DB db, next goji.Handler) goji.HandlerFunc {
|
||||
return recoverHandler(restCheckAppDisabledHandler(DB, restAuthenticationHandler(DB, next)))
|
||||
}
|
||||
|
||||
func newPostEventsHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &postEventsHandler{DB})
|
||||
}
|
||||
|
||||
type postEventsHandler struct{ DB db }
|
||||
|
||||
// ServeHTTPC An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
|
||||
@@ -141,8 +122,8 @@ type postEventsHandler struct{ DB db }
|
||||
// Response is an empty JSON hash.
|
||||
//
|
||||
// POST /apps/{app_id}/events
|
||||
func (h *postEventsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
func (h *postEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
|
||||
app, err := h.DB.GetAppByAppID(appID)
|
||||
|
||||
@@ -187,10 +168,6 @@ func (h *postEventsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrite
|
||||
w.Write([]byte("{}"))
|
||||
}
|
||||
|
||||
func newGetChannelsHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelsHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelsHandler struct{ DB db }
|
||||
|
||||
// Allows fetching a hash of occupied channels (optionally filtered by prefix),
|
||||
@@ -212,10 +189,10 @@ type getChannelsHandler struct{ DB db }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels
|
||||
func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
func (h *getChannelsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
filter := query.Get("filter_by_prefix")
|
||||
info := query.Get("info")
|
||||
|
||||
@@ -282,10 +259,6 @@ func (h *getChannelsHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrit
|
||||
}
|
||||
}
|
||||
|
||||
func newGetChannelHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelHandler struct{ DB db }
|
||||
|
||||
// Fetch info for one channel
|
||||
@@ -298,19 +271,19 @@ type getChannelHandler struct{ DB db }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}
|
||||
func (h *getChannelHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
func (h *getChannelHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
app, err := h.DB.GetAppByAppID(appID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
channelName := pat.Param(ctx, "channel_name")
|
||||
channelName := chi.URLParam(r, "channel_name")
|
||||
|
||||
// Channel name could not be empty
|
||||
if strings.TrimSpace(channelName) == "" {
|
||||
@@ -376,10 +349,6 @@ func (h *getChannelHandler) ServeHTTPC(ctx context.Context, w http.ResponseWrite
|
||||
}
|
||||
}
|
||||
|
||||
func newGetChannelUsersHandler(DB db) goji.HandlerFunc {
|
||||
return commonHandlers(DB, &getChannelUsersHandler{DB})
|
||||
}
|
||||
|
||||
type getChannelUsersHandler struct{ DB db }
|
||||
|
||||
// Allowed only for presence-channels
|
||||
@@ -393,9 +362,9 @@ type getChannelUsersHandler struct{ DB db }
|
||||
// }
|
||||
//
|
||||
// GET /apps/{app_id}/channels/{channel_name}/users
|
||||
func (h *getChannelUsersHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
appID := pat.Param(ctx, "app_id")
|
||||
channelName := pat.Param(ctx, "channel_name")
|
||||
func (h *getChannelUsersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "app_id")
|
||||
channelName := chi.URLParam(r, "channel_name")
|
||||
|
||||
isPresence := utils.IsPresenceChannel(channelName)
|
||||
|
||||
|
||||
+26
-21
@@ -1,15 +1,14 @@
|
||||
package ipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goji.io/pattern"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"github.com/pressly/chi"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,14 +40,15 @@ func init() {
|
||||
func Test_getChannels_all(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -67,14 +67,15 @@ func Test_getChannels_all(t *testing.T) {
|
||||
func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -94,14 +95,15 @@ func Test_getChannels_filter_by_presence_prefix(t *testing.T) {
|
||||
func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -133,14 +135,15 @@ func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) {
|
||||
func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest)
|
||||
@@ -150,14 +153,15 @@ func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T)
|
||||
func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
@@ -183,14 +187,15 @@ func Test_getChannels_filter_by_public_prefix(t *testing.T) {
|
||||
func Test_getChannels_filter_by_private_prefix(t *testing.T) {
|
||||
appID := testApp.AppID
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, pattern.Variable("app_id"), appID)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("app_id", appID)
|
||||
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil)
|
||||
r = r.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := &getChannelsHandler{database}
|
||||
handler.ServeHTTPC(ctx, w, r)
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK)
|
||||
|
||||
+17
-12
@@ -11,11 +11,9 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"goji.io/pat"
|
||||
|
||||
goji "goji.io"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"github.com/pressly/chi"
|
||||
"github.com/pressly/chi/middleware"
|
||||
)
|
||||
|
||||
// Start Parse the configuration file and starts the ipe server
|
||||
@@ -47,20 +45,27 @@ func Start(filename string) {
|
||||
db.AddApp(newAppFromConfig(a))
|
||||
}
|
||||
|
||||
router := goji.NewMux()
|
||||
router.HandleFuncC(pat.Post("/apps/:app_id/events"), newPostEventsHandler(db))
|
||||
router.HandleFuncC(pat.Get("/apps/:app_id/channels"), newGetChannelsHandler(db))
|
||||
router.HandleFuncC(pat.Get("/apps/:app_id/channels/:channel_name"), newGetChannelHandler(db))
|
||||
router.HandleFuncC(pat.Get("/apps/:app_id/channels/:channel_name/users"), newGetChannelUsersHandler(db))
|
||||
router.HandleC(pat.Get("/app/:key"), newWebsocketHandler(db))
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Handle("/app/:key", &websocketHandler{db})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(checkAppDisabled(db))
|
||||
r.Use(authenticationHandler(db))
|
||||
|
||||
r.Handle("/apps/:app_id/events", &postEventsHandler{db})
|
||||
r.Handle("/apps/:app_id/channels", &getChannelsHandler{db})
|
||||
r.Handle("/apps/:app_id/channels/:channel_name", &getChannelHandler{db})
|
||||
r.Handle("/apps/:app_id/channels/:channel_name/users", &getChannelUsersHandler{db})
|
||||
})
|
||||
|
||||
if conf.SSL {
|
||||
go func() {
|
||||
log.Infof("Starting HTTPS service on %s ...", conf.SSLHost)
|
||||
log.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, router))
|
||||
log.Fatal(http.ListenAndServeTLS(conf.SSLHost, conf.SSLCertFile, conf.SSLKeyFile, r))
|
||||
}()
|
||||
}
|
||||
|
||||
log.Infof("Starting HTTP service on %s ...", conf.Host)
|
||||
log.Fatal(http.ListenAndServe(conf.Host, router))
|
||||
log.Fatal(http.ListenAndServe(conf.Host, r))
|
||||
}
|
||||
|
||||
+3
-11
@@ -12,13 +12,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
goji "goji.io"
|
||||
|
||||
"goji.io/pat"
|
||||
|
||||
log "github.com/golang/glog"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/net/context"
|
||||
"github.com/pressly/chi"
|
||||
|
||||
"github.com/dimiro1/ipe/utils"
|
||||
)
|
||||
@@ -245,16 +241,12 @@ func emitWSError(err error, conn *websocket.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func newWebsocketHandler(DB db) goji.Handler {
|
||||
return &websocketHandler{DB}
|
||||
}
|
||||
|
||||
type websocketHandler struct {
|
||||
DB db
|
||||
}
|
||||
|
||||
// Websocket GET /app/{key}
|
||||
func (h *websocketHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
func (h *websocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
@@ -267,7 +259,7 @@ func (h *websocketHandler) ServeHTTPC(ctx context.Context, w http.ResponseWriter
|
||||
return
|
||||
}
|
||||
|
||||
appKey := pat.Param(ctx, "key")
|
||||
appKey := chi.URLParam(r, "key")
|
||||
|
||||
app, err := h.DB.GetAppByKey(appKey)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user