mirror of
https://github.com/astaxie/beego.git
synced 2024-12-28 17:45:47 +00:00
Merge branch 'develop' of https://github.com/astaxie/beego into develop
This commit is contained in:
commit
0d0d87f600
@ -67,6 +67,7 @@ func oldMap() map[string]interface{} {
|
||||
m["BConfig.WebConfig.Session.SessionDomain"] = BConfig.WebConfig.Session.SessionDomain
|
||||
m["BConfig.WebConfig.Session.SessionDisableHTTPOnly"] = BConfig.WebConfig.Session.SessionDisableHTTPOnly
|
||||
m["BConfig.Log.AccessLogs"] = BConfig.Log.AccessLogs
|
||||
m["BConfig.Log.EnableStaticLogs"] = BConfig.Log.EnableStaticLogs
|
||||
m["BConfig.Log.AccessLogsFormat"] = BConfig.Log.AccessLogsFormat
|
||||
m["BConfig.Log.FileLineNum"] = BConfig.Log.FileLineNum
|
||||
m["BConfig.Log.Outputs"] = BConfig.Log.Outputs
|
||||
|
@ -106,6 +106,7 @@ type SessionConfig struct {
|
||||
// LogConfig holds Log related config
|
||||
type LogConfig struct {
|
||||
AccessLogs bool
|
||||
EnableStaticLogs bool //log static files requests default: false
|
||||
AccessLogsFormat string //access log format: JSON_FORMAT, APACHE_FORMAT or empty string
|
||||
FileLineNum bool
|
||||
Outputs map[string]string // Store Adaptor : config
|
||||
@ -182,6 +183,11 @@ func recoverPanic(ctx *context.Context) {
|
||||
if BConfig.RunMode == DEV && BConfig.EnableErrorsRender {
|
||||
showErr(err, ctx, stack)
|
||||
}
|
||||
if ctx.Output.Status != 0 {
|
||||
ctx.ResponseWriter.WriteHeader(ctx.Output.Status)
|
||||
} else {
|
||||
ctx.ResponseWriter.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,6 +253,7 @@ func newBConfig() *Config {
|
||||
},
|
||||
Log: LogConfig{
|
||||
AccessLogs: false,
|
||||
EnableStaticLogs: false,
|
||||
AccessLogsFormat: "APACHE_FORMAT",
|
||||
FileLineNum: true,
|
||||
Outputs: map[string]string{"console": ""},
|
||||
|
@ -272,7 +272,22 @@ func (c *Controller) viewPath() string {
|
||||
|
||||
// Redirect sends the redirection response to url with status code.
|
||||
func (c *Controller) Redirect(url string, code int) {
|
||||
logAccess(c.Ctx, nil, code)
|
||||
c.Ctx.Redirect(code, url)
|
||||
panic(ErrAbort)
|
||||
}
|
||||
|
||||
// Set the data depending on the accepted
|
||||
func (c *Controller) SetData(data interface{}) {
|
||||
accept := c.Ctx.Input.Header("Accept")
|
||||
switch accept {
|
||||
case applicationJSON:
|
||||
c.Data["json"] = data
|
||||
case applicationXML, textXML:
|
||||
c.Data["xml"] = data
|
||||
default:
|
||||
c.Data["json"] = data
|
||||
}
|
||||
}
|
||||
|
||||
// Abort stops controller handler and show the error data if code is defined in ErrorMap or code string.
|
||||
|
10
error.go
10
error.go
@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
errorTypeHandler = iota
|
||||
errorTypeHandler = iota
|
||||
errorTypeController
|
||||
)
|
||||
|
||||
@ -93,11 +93,6 @@ func showErr(err interface{}, ctx *context.Context, stack string) {
|
||||
"BeegoVersion": VERSION,
|
||||
"GoVersion": runtime.Version(),
|
||||
}
|
||||
if ctx.Output.Status != 0 {
|
||||
ctx.ResponseWriter.WriteHeader(ctx.Output.Status)
|
||||
} else {
|
||||
ctx.ResponseWriter.WriteHeader(500)
|
||||
}
|
||||
t.Execute(ctx.ResponseWriter, data)
|
||||
}
|
||||
|
||||
@ -439,6 +434,9 @@ func exception(errCode string, ctx *context.Context) {
|
||||
}
|
||||
|
||||
func executeError(err *errorInfo, ctx *context.Context, code int) {
|
||||
//make sure to log the error in the access log
|
||||
logAccess(ctx, nil, code)
|
||||
|
||||
if err.errorType == errorTypeHandler {
|
||||
ctx.ResponseWriter.WriteHeader(code)
|
||||
err.handler(ctx.ResponseWriter, ctx.Request)
|
||||
|
@ -16,48 +16,57 @@ As of now this logs support console, file,smtp and conn.
|
||||
|
||||
First you must import it
|
||||
|
||||
import (
|
||||
"github.com/astaxie/beego/logs"
|
||||
)
|
||||
```golang
|
||||
import (
|
||||
"github.com/astaxie/beego/logs"
|
||||
)
|
||||
```
|
||||
|
||||
Then init a Log (example with console adapter)
|
||||
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("console", "")
|
||||
```golang
|
||||
log := logs.NewLogger(10000)
|
||||
log.SetLogger("console", "")
|
||||
```
|
||||
|
||||
> the first params stand for how many channel
|
||||
|
||||
Use it like this:
|
||||
|
||||
log.Trace("trace")
|
||||
log.Info("info")
|
||||
log.Warn("warning")
|
||||
log.Debug("debug")
|
||||
log.Critical("critical")
|
||||
Use it like this:
|
||||
|
||||
```golang
|
||||
log.Trace("trace")
|
||||
log.Info("info")
|
||||
log.Warn("warning")
|
||||
log.Debug("debug")
|
||||
log.Critical("critical")
|
||||
```
|
||||
|
||||
## File adapter
|
||||
|
||||
Configure file adapter like this:
|
||||
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test.log"}`)
|
||||
|
||||
```golang
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("file", `{"filename":"test.log"}`)
|
||||
```
|
||||
|
||||
## Conn adapter
|
||||
|
||||
Configure like this:
|
||||
|
||||
log := NewLogger(1000)
|
||||
log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`)
|
||||
log.Info("info")
|
||||
|
||||
```golang
|
||||
log := NewLogger(1000)
|
||||
log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`)
|
||||
log.Info("info")
|
||||
```
|
||||
|
||||
## Smtp adapter
|
||||
|
||||
Configure like this:
|
||||
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`)
|
||||
log.Critical("sendmail critical")
|
||||
time.Sleep(time.Second * 30)
|
||||
```golang
|
||||
log := NewLogger(10000)
|
||||
log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`)
|
||||
log.Critical("sendmail critical")
|
||||
time.Sleep(time.Second * 30)
|
||||
```
|
||||
|
@ -16,6 +16,7 @@ package logs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
@ -63,9 +64,7 @@ func disableEscapeHTML(i interface{}) {
|
||||
// AccessLog - Format and print access log.
|
||||
func AccessLog(r *AccessLogRecord, format string) {
|
||||
var msg string
|
||||
|
||||
switch format {
|
||||
|
||||
case apacheFormat:
|
||||
timeFormatted := r.RequestTime.Format("02/Jan/2006 03:04:05")
|
||||
msg = fmt.Sprintf(apacheFormatPattern, r.RemoteAddr, timeFormatted, r.Request, r.Status, r.BodyBytesSent,
|
||||
@ -80,6 +79,5 @@ func AccessLog(r *AccessLogRecord, format string) {
|
||||
msg = string(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
beeLogger.Debug(msg)
|
||||
beeLogger.writeMsg(levelLoggerImpl, strings.TrimSpace(msg))
|
||||
}
|
||||
|
@ -198,7 +198,11 @@ func (o *querySet) PrepareInsert() (Inserter, error) {
|
||||
// query all data and map to containers.
|
||||
// cols means the columns when querying.
|
||||
func (o *querySet) All(container interface{}, cols ...string) (int64, error) {
|
||||
return o.orm.alias.DbBaser.ReadBatch(o.orm.db, o, o.mi, o.cond, container, o.orm.alias.TZ, cols)
|
||||
num, err := o.orm.alias.DbBaser.ReadBatch(o.orm.db, o, o.mi, o.cond, container, o.orm.alias.TZ, cols)
|
||||
if num == 0 {
|
||||
return 0, ErrNoRows
|
||||
}
|
||||
return num, err
|
||||
}
|
||||
|
||||
// query one row data and map to containers.
|
||||
|
92
router.go
92
router.go
@ -43,7 +43,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
routerTypeBeego = iota
|
||||
routerTypeBeego = iota
|
||||
routerTypeRESTFul
|
||||
routerTypeHandler
|
||||
)
|
||||
@ -877,13 +877,15 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
Admin:
|
||||
//admin module record QPS
|
||||
//admin module record QPS
|
||||
|
||||
statusCode := context.ResponseWriter.Status
|
||||
if statusCode == 0 {
|
||||
statusCode = 200
|
||||
}
|
||||
|
||||
logAccess(context, &startTime, statusCode)
|
||||
|
||||
if BConfig.Listen.EnableAdmin {
|
||||
timeDur := time.Since(startTime)
|
||||
pattern := ""
|
||||
@ -900,49 +902,30 @@ Admin:
|
||||
}
|
||||
}
|
||||
|
||||
if BConfig.RunMode == DEV || BConfig.Log.AccessLogs {
|
||||
timeDur := time.Since(startTime)
|
||||
if BConfig.RunMode == DEV && !BConfig.Log.AccessLogs {
|
||||
var devInfo string
|
||||
|
||||
timeDur := time.Since(startTime)
|
||||
iswin := (runtime.GOOS == "windows")
|
||||
statusColor := logs.ColorByStatus(iswin, statusCode)
|
||||
methodColor := logs.ColorByMethod(iswin, r.Method)
|
||||
resetColor := logs.ColorByMethod(iswin, "")
|
||||
if BConfig.Log.AccessLogsFormat != "" {
|
||||
record := &logs.AccessLogRecord{
|
||||
RemoteAddr: context.Input.IP(),
|
||||
RequestTime: startTime,
|
||||
RequestMethod: r.Method,
|
||||
Request: fmt.Sprintf("%s %s %s", r.Method, r.RequestURI, r.Proto),
|
||||
ServerProtocol: r.Proto,
|
||||
Host: r.Host,
|
||||
Status: statusCode,
|
||||
ElapsedTime: timeDur,
|
||||
HTTPReferrer: r.Header.Get("Referer"),
|
||||
HTTPUserAgent: r.Header.Get("User-Agent"),
|
||||
RemoteUser: r.Header.Get("Remote-User"),
|
||||
BodyBytesSent: 0, //@todo this one is missing!
|
||||
}
|
||||
logs.AccessLog(record, BConfig.Log.AccessLogsFormat)
|
||||
} else {
|
||||
if findRouter {
|
||||
if routerInfo != nil {
|
||||
devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s r:%s", context.Input.IP(), statusColor, statusCode,
|
||||
resetColor, timeDur.String(), "match", methodColor, r.Method, resetColor, r.URL.Path,
|
||||
routerInfo.pattern)
|
||||
} else {
|
||||
devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s", context.Input.IP(), statusColor, statusCode, resetColor,
|
||||
timeDur.String(), "match", methodColor, r.Method, resetColor, r.URL.Path)
|
||||
}
|
||||
if findRouter {
|
||||
if routerInfo != nil {
|
||||
devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s r:%s", context.Input.IP(), statusColor, statusCode,
|
||||
resetColor, timeDur.String(), "match", methodColor, r.Method, resetColor, r.URL.Path,
|
||||
routerInfo.pattern)
|
||||
} else {
|
||||
devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s", context.Input.IP(), statusColor, statusCode, resetColor,
|
||||
timeDur.String(), "nomatch", methodColor, r.Method, resetColor, r.URL.Path)
|
||||
}
|
||||
if iswin {
|
||||
logs.W32Debug(devInfo)
|
||||
} else {
|
||||
logs.Debug(devInfo)
|
||||
timeDur.String(), "match", methodColor, r.Method, resetColor, r.URL.Path)
|
||||
}
|
||||
} else {
|
||||
devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s", context.Input.IP(), statusColor, statusCode, resetColor,
|
||||
timeDur.String(), "nomatch", methodColor, r.Method, resetColor, r.URL.Path)
|
||||
}
|
||||
if iswin {
|
||||
logs.W32Debug(devInfo)
|
||||
} else {
|
||||
logs.Debug(devInfo)
|
||||
}
|
||||
}
|
||||
// Call WriteHeader if status code has been set changed
|
||||
@ -991,3 +974,38 @@ func toURL(params map[string]string) string {
|
||||
}
|
||||
return strings.TrimRight(u, "&")
|
||||
}
|
||||
|
||||
func logAccess(ctx *beecontext.Context, startTime *time.Time, statusCode int) {
|
||||
//Skip logging if AccessLogs config is false
|
||||
if !BConfig.Log.AccessLogs {
|
||||
return
|
||||
}
|
||||
//Skip logging static requests unless EnableStaticLogs config is true
|
||||
if !BConfig.Log.EnableStaticLogs && DefaultAccessLogFilter.Filter(ctx) {
|
||||
return
|
||||
}
|
||||
var (
|
||||
requestTime time.Time
|
||||
elapsedTime time.Duration
|
||||
r = ctx.Request
|
||||
)
|
||||
if startTime != nil {
|
||||
requestTime = *startTime
|
||||
elapsedTime = time.Since(*startTime)
|
||||
}
|
||||
record := &logs.AccessLogRecord{
|
||||
RemoteAddr: ctx.Input.IP(),
|
||||
RequestTime: requestTime,
|
||||
RequestMethod: r.Method,
|
||||
Request: fmt.Sprintf("%s %s %s", r.Method, r.RequestURI, r.Proto),
|
||||
ServerProtocol: r.Proto,
|
||||
Host: r.Host,
|
||||
Status: statusCode,
|
||||
ElapsedTime: elapsedTime,
|
||||
HTTPReferrer: r.Header.Get("Referer"),
|
||||
HTTPUserAgent: r.Header.Get("User-Agent"),
|
||||
RemoteUser: r.Header.Get("Remote-User"),
|
||||
BodyBytesSent: 0, //@todo this one is missing!
|
||||
}
|
||||
logs.AccessLog(record, BConfig.Log.AccessLogsFormat)
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/session"
|
||||
|
||||
@ -118,8 +119,8 @@ type Provider struct {
|
||||
}
|
||||
|
||||
// SessionInit init redis session
|
||||
// savepath like redis server addr,pool size,password,dbnum
|
||||
// e.g. 127.0.0.1:6379,100,astaxie,0
|
||||
// savepath like redis server addr,pool size,password,dbnum,IdleTimeout second
|
||||
// e.g. 127.0.0.1:6379,100,astaxie,0,30
|
||||
func (rp *Provider) SessionInit(maxlifetime int64, savePath string) error {
|
||||
rp.maxlifetime = maxlifetime
|
||||
configs := strings.Split(savePath, ",")
|
||||
@ -149,6 +150,13 @@ func (rp *Provider) SessionInit(maxlifetime int64, savePath string) error {
|
||||
} else {
|
||||
rp.dbNum = 0
|
||||
}
|
||||
var idleTimeout time.Duration = 0
|
||||
if len(configs) > 4 {
|
||||
timeout, err := strconv.Atoi(configs[4])
|
||||
if err == nil && timeout > 0 {
|
||||
idleTimeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
}
|
||||
rp.poollist = &redis.Pool{
|
||||
Dial: func() (redis.Conn, error) {
|
||||
c, err := redis.Dial("tcp", rp.savePath)
|
||||
@ -171,9 +179,11 @@ func (rp *Provider) SessionInit(maxlifetime int64, savePath string) error {
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
MaxIdle: rp.poolsize,
|
||||
MaxIdle: rp.poolsize,
|
||||
}
|
||||
|
||||
rp.poollist.IdleTimeout = idleTimeout
|
||||
|
||||
return rp.poollist.Get().Err()
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ func serverStaticRouter(ctx *context.Context) {
|
||||
if enableCompress {
|
||||
acceptEncoding = context.ParseEncoding(ctx.Request)
|
||||
}
|
||||
b, n, sch, err := openFile(filePath, fileInfo, acceptEncoding)
|
||||
b, n, sch, reader, err := openFile(filePath, fileInfo, acceptEncoding)
|
||||
if err != nil {
|
||||
if BConfig.RunMode == DEV {
|
||||
logs.Warn("Can't compress the file:", filePath, err)
|
||||
@ -89,47 +89,53 @@ func serverStaticRouter(ctx *context.Context) {
|
||||
ctx.Output.Header("Content-Length", strconv.FormatInt(sch.size, 10))
|
||||
}
|
||||
|
||||
http.ServeContent(ctx.ResponseWriter, ctx.Request, filePath, sch.modTime, sch)
|
||||
http.ServeContent(ctx.ResponseWriter, ctx.Request, filePath, sch.modTime, reader)
|
||||
}
|
||||
|
||||
type serveContentHolder struct {
|
||||
*bytes.Reader
|
||||
data []byte
|
||||
modTime time.Time
|
||||
size int64
|
||||
encoding string
|
||||
}
|
||||
|
||||
type serveContentReader struct {
|
||||
*bytes.Reader
|
||||
}
|
||||
|
||||
var (
|
||||
staticFileMap = make(map[string]*serveContentHolder)
|
||||
mapLock sync.RWMutex
|
||||
)
|
||||
|
||||
func openFile(filePath string, fi os.FileInfo, acceptEncoding string) (bool, string, *serveContentHolder, error) {
|
||||
func openFile(filePath string, fi os.FileInfo, acceptEncoding string) (bool, string, *serveContentHolder, *serveContentReader, error) {
|
||||
mapKey := acceptEncoding + ":" + filePath
|
||||
mapLock.RLock()
|
||||
mapFile := staticFileMap[mapKey]
|
||||
mapLock.RUnlock()
|
||||
if isOk(mapFile, fi) {
|
||||
return mapFile.encoding != "", mapFile.encoding, mapFile, nil
|
||||
reader := &serveContentReader{Reader: bytes.NewReader(mapFile.data)}
|
||||
return mapFile.encoding != "", mapFile.encoding, mapFile, reader, nil
|
||||
}
|
||||
mapLock.Lock()
|
||||
defer mapLock.Unlock()
|
||||
if mapFile = staticFileMap[mapKey]; !isOk(mapFile, fi) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, "", nil, nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
var bufferWriter bytes.Buffer
|
||||
_, n, err := context.WriteFile(acceptEncoding, &bufferWriter, file)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, "", nil, nil, err
|
||||
}
|
||||
mapFile = &serveContentHolder{Reader: bytes.NewReader(bufferWriter.Bytes()), modTime: fi.ModTime(), size: int64(bufferWriter.Len()), encoding: n}
|
||||
mapFile = &serveContentHolder{data: bufferWriter.Bytes(), modTime: fi.ModTime(), size: int64(bufferWriter.Len()), encoding: n}
|
||||
staticFileMap[mapKey] = mapFile
|
||||
}
|
||||
|
||||
return mapFile.encoding != "", mapFile.encoding, mapFile, nil
|
||||
reader := &serveContentReader{Reader: bytes.NewReader(mapFile.data)}
|
||||
return mapFile.encoding != "", mapFile.encoding, mapFile, reader, nil
|
||||
}
|
||||
|
||||
func isOk(s *serveContentHolder, fi os.FileInfo) bool {
|
||||
|
@ -16,7 +16,7 @@ var licenseFile = filepath.Join(currentWorkDir, "LICENSE")
|
||||
|
||||
func testOpenFile(encoding string, content []byte, t *testing.T) {
|
||||
fi, _ := os.Stat(licenseFile)
|
||||
b, n, sch, err := openFile(licenseFile, fi, encoding)
|
||||
b, n, sch, reader, err := openFile(licenseFile, fi, encoding)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
@ -24,7 +24,7 @@ func testOpenFile(encoding string, content []byte, t *testing.T) {
|
||||
|
||||
t.Log("open static file encoding "+n, b)
|
||||
|
||||
assetOpenFileAndContent(sch, content, t)
|
||||
assetOpenFileAndContent(sch, reader, content, t)
|
||||
}
|
||||
func TestOpenStaticFile_1(t *testing.T) {
|
||||
file, _ := os.Open(licenseFile)
|
||||
@ -53,13 +53,13 @@ func TestOpenStaticFileDeflate_1(t *testing.T) {
|
||||
testOpenFile("deflate", content, t)
|
||||
}
|
||||
|
||||
func assetOpenFileAndContent(sch *serveContentHolder, content []byte, t *testing.T) {
|
||||
func assetOpenFileAndContent(sch *serveContentHolder, reader *serveContentReader, content []byte, t *testing.T) {
|
||||
t.Log(sch.size, len(content))
|
||||
if sch.size != int64(len(content)) {
|
||||
t.Log("static content file size not same")
|
||||
t.Fail()
|
||||
}
|
||||
bs, _ := ioutil.ReadAll(sch)
|
||||
bs, _ := ioutil.ReadAll(reader)
|
||||
for i, v := range content {
|
||||
if v != bs[i] {
|
||||
t.Log("content not same")
|
||||
|
@ -365,10 +365,10 @@ func (v *Validation) Valid(obj interface{}) (b bool, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var hasReuired bool
|
||||
var hasRequired bool
|
||||
for _, vf := range vfs {
|
||||
if vf.Name == "Required" {
|
||||
hasReuired = true
|
||||
hasRequired = true
|
||||
}
|
||||
|
||||
currentField := objV.Field(i).Interface()
|
||||
@ -382,7 +382,7 @@ func (v *Validation) Valid(obj interface{}) (b bool, err error) {
|
||||
|
||||
|
||||
chk := Required{""}.IsSatisfied(currentField)
|
||||
if !hasReuired && v.RequiredFirst && !chk {
|
||||
if !hasRequired && v.RequiredFirst && !chk {
|
||||
if _, ok := CanSkipFuncs[vf.Name]; ok {
|
||||
continue
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user