diff --git a/app/rest/admin.go b/app/rest/admin.go index fd89d3f3..ec68116f 100644 --- a/app/rest/admin.go +++ b/app/rest/admin.go @@ -14,6 +14,7 @@ import ( "github.com/umputun/remark/app/migrator" "github.com/umputun/remark/app/rest/auth" + "github.com/umputun/remark/app/rest/common" "github.com/umputun/remark/app/store" ) @@ -46,7 +47,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { err := a.dataService.Delete(locator, id) if err != nil { log.Printf("[WARN] can't delete comment, %s", err) - httpError(w, r, http.StatusInternalServerError, err, "can't delete comment") + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't delete comment") return } a.respCache.Flush() @@ -61,7 +62,7 @@ func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) { blockStatus := r.URL.Query().Get("block") == "1" if err := a.dataService.SetBlock(store.Locator{SiteID: siteID}, userID, blockStatus); err != nil { - httpError(w, r, http.StatusBadRequest, err, "can't set blocking status") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set blocking status") return } a.respCache.Flush() @@ -76,7 +77,7 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) { pinStatus := r.URL.Query().Get("pin") == "1" if err := a.dataService.SetPin(locator, commentID, pinStatus); err != nil { - httpError(w, r, http.StatusBadRequest, err, "can't set pin status") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set pin status") return } a.respCache.Flush() @@ -97,7 +98,7 @@ func (a *admin) exportCtrl(w http.ResponseWriter, r *http.Request) { } if err := a.exporter.Export(writer, siteID); err != nil { - httpError(w, r, http.StatusInternalServerError, err, "export failed") + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed") } } @@ -106,7 +107,7 @@ func (a *admin) exportCtrl(w http.ResponseWriter, r *http.Request) { func (a *admin) importCtrl(w http.ResponseWriter, r *http.Request) { siteID := r.URL.Query().Get("site") if err := a.importer.Import(r.Body, siteID); err != nil { - httpError(w, r, http.StatusBadRequest, err, "import failed") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "import failed") } a.respCache.Flush() } diff --git a/app/rest/auth/auth.go b/app/rest/auth/auth.go index 8fa20685..033f3dd8 100644 --- a/app/rest/auth/auth.go +++ b/app/rest/auth/auth.go @@ -16,6 +16,7 @@ import ( "github.com/gorilla/sessions" "golang.org/x/oauth2" + "github.com/umputun/remark/app/rest/common" "github.com/umputun/remark/app/store" ) @@ -85,7 +86,7 @@ func (p Provider) LoginHandler(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] login, %+v", session.Values) if err := session.Save(r, w); err != nil { - http.Error(w, fmt.Sprintf("failed to save start, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save state") return } @@ -100,7 +101,7 @@ func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) { session, err := p.Get(r, "remark") if err != nil { - http.Error(w, fmt.Sprintf("failed to get session, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to get session") return } @@ -114,14 +115,14 @@ func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] auth, %+v", session.Values) tok, err := p.conf.Exchange(context.Background(), r.URL.Query().Get("code")) if err != nil { - http.Error(w, fmt.Sprintf("exchange failed, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "exchange failed") return } client := p.conf.Client(context.Background(), tok) uinfo, err := client.Get(p.InfoURL) if err != nil { - http.Error(w, fmt.Sprintf("failed to get client info via %s, %s", p.InfoURL, err), http.StatusBadRequest) + common.SendErrorJSON(w, r, http.StatusBadRequest, err, fmt.Sprintf("failed to get client info via %s", p.InfoURL)) return } @@ -133,20 +134,20 @@ func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) { data, err := ioutil.ReadAll(uinfo.Body) if err != nil { - http.Error(w, fmt.Sprintf("failed to read user info, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to read user info") return } jData := map[string]interface{}{} if e := json.Unmarshal(data, &jData); e != nil { - http.Error(w, fmt.Sprintf("failed to unmarshal user info, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info") return } log.Printf("[DEBUG] got raw user info %+v", jData) session.Values["uinfo"] = p.MapUser(jData) if err = session.Save(r, w); err != nil { - http.Error(w, fmt.Sprintf("failed to save user info, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save user info") return } @@ -165,7 +166,7 @@ func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) { func (p Provider) LogoutHandler(w http.ResponseWriter, r *http.Request) { session, err := p.Get(r, "remark") if err != nil { - http.Error(w, fmt.Sprintf("failed to get session, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "failed to get session") return } @@ -178,7 +179,7 @@ func (p Provider) LogoutHandler(w http.ResponseWriter, r *http.Request) { delete(session.Values, "state") if err = session.Save(r, w); err != nil { - http.Error(w, fmt.Sprintf("failed to reset user info, %s", err), http.StatusInternalServerError) + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to reset user info") return } log.Printf("[DEBUG] logout, %+v", session.Values) diff --git a/app/rest/auth/middleware.go b/app/rest/auth/middleware.go index 68e7abb7..bdf2b511 100644 --- a/app/rest/auth/middleware.go +++ b/app/rest/auth/middleware.go @@ -2,16 +2,14 @@ package auth import ( "context" - "errors" "net/http" "github.com/gorilla/sessions" + "github.com/umputun/remark/app/rest/common" "github.com/umputun/remark/app/store" ) -type contextKey string - // Mode defines behavior of Auth middleware type Mode int @@ -49,7 +47,7 @@ func Auth(sessionStore *sessions.FilesystemStore, admins []string, modes []Mode) if inModes(Developer) { user := devUser ctx := r.Context() - ctx = context.WithValue(ctx, contextKey("user"), user) + ctx = context.WithValue(ctx, common.ContextKey("user"), user) r = r.WithContext(ctx) h.ServeHTTP(w, r) return @@ -82,7 +80,7 @@ func Auth(sessionStore *sessions.FilesystemStore, admins []string, modes []Mode) } ctx := r.Context() - ctx = context.WithValue(ctx, contextKey("user"), user) + ctx = context.WithValue(ctx, common.ContextKey("user"), user) r = r.WithContext(ctx) } h.ServeHTTP(w, r) @@ -96,7 +94,7 @@ func Auth(sessionStore *sessions.FilesystemStore, admins []string, modes []Mode) func AdminOnly(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - user, err := GetUserInfo(r) + user, err := common.GetUserInfo(r) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return @@ -111,18 +109,3 @@ func AdminOnly(next http.Handler) http.Handler { } return http.HandlerFunc(fn) } - -// GetUserInfo extracts user, or and token from request's context -func GetUserInfo(r *http.Request) (user store.User, err error) { - - ctx := r.Context() - if ctx == nil { - return store.User{}, errors.New("no info about user") - } - - if u, ok := ctx.Value(contextKey("user")).(store.User); ok { - return u, nil - } - - return store.User{}, errors.New("user can't be parsed") -} diff --git a/app/rest/common/http_errors.go b/app/rest/common/http_errors.go new file mode 100644 index 00000000..b90b5f14 --- /dev/null +++ b/app/rest/common/http_errors.go @@ -0,0 +1,30 @@ +package common + +import ( + "log" + "net/http" + "strings" + + "github.com/go-chi/render" +) + +// SendErrorJSON makes {error: blah, details: blah} json body and responds with error code +func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, details string) { + logDetails(r, code, err, details) + render.Status(r, code) + render.JSON(w, r, map[string]interface{}{"error": err.Error(), "details": details}) +} + +// SendErrorText with simple text body and responds with error code +func SendErrorText(w http.ResponseWriter, r *http.Request, code int, text string) { + render.Status(r, code) + render.PlainText(w, r, text) +} + +func logDetails(r *http.Request, code int, err error, details string) { + uinfoStr := "" + if user, err := GetUserInfo(r); err == nil { + uinfoStr = user.Name + "/" + user.ID + " - " + } + log.Printf("[DEBUG] %s - %v - %d - %s%s - %s", details, err, code, uinfoStr, strings.Split(r.RemoteAddr, ":")[0], r.URL) +} diff --git a/app/rest/common/user.go b/app/rest/common/user.go new file mode 100644 index 00000000..aa9222e1 --- /dev/null +++ b/app/rest/common/user.go @@ -0,0 +1,26 @@ +package common + +import ( + "errors" + "net/http" + + "github.com/umputun/remark/app/store" +) + +// ContextKey is a type to match on context +type ContextKey string + +// GetUserInfo returns user from request context +func GetUserInfo(r *http.Request) (user store.User, err error) { + + ctx := r.Context() + if ctx == nil { + return store.User{}, errors.New("no info about user") + } + + if u, ok := ctx.Value(ContextKey("user")).(store.User); ok { + return u, nil + } + + return store.User{}, errors.New("user can't be parsed") +} diff --git a/app/rest/middleware.go b/app/rest/middleware.go index 3c0e38ea..576b2d7c 100644 --- a/app/rest/middleware.go +++ b/app/rest/middleware.go @@ -16,8 +16,7 @@ import ( "github.com/didip/tollbooth" "github.com/go-chi/chi/middleware" "github.com/go-chi/render" - - "github.com/umputun/remark/app/rest/auth" + "github.com/umputun/remark/app/rest/common" ) var org = "Umputun" @@ -162,7 +161,7 @@ func Logger(flags ...LoggerFlag) func(http.Handler) http.Handler { } if inFlags(LogUser) { - u, err := auth.GetUserInfo(r) + u, err := common.GetUserInfo(r) if err == nil && u.Name != "" { user = fmt.Sprintf(" - %s %q", u.ID, u.Name) } diff --git a/app/rest/server.go b/app/rest/server.go index fb24d660..66de12e5 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -20,6 +20,7 @@ import ( "github.com/umputun/remark/app/migrator" "github.com/umputun/remark/app/rest/auth" + "github.com/umputun/remark/app/rest/common" "github.com/umputun/remark/app/rest/format" "github.com/umputun/remark/app/store" ) @@ -113,13 +114,13 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) { comment := store.Comment{} if err := render.DecodeJSON(r.Body, &comment); err != nil { log.Printf("[WARN] can't bind request %s", err) - httpError(w, r, http.StatusBadRequest, err, "can't bind comment") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") return } - user, err := auth.GetUserInfo(r) + user, err := common.GetUserInfo(r) if err != nil { // this not suppose to happen (handled by Auth), just dbl-check - httpError(w, r, http.StatusUnauthorized, err, "can't get user info") + common.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") return } @@ -133,14 +134,14 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) { // check if user blocked if s.mod.checkBlocked(store.Locator{}, comment.User) { log.Printf("[WARN] user %s rejected (blocked)", err) - httpError(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked") + common.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked") return } id, err := s.DataService.Create(comment) if err != nil { log.Printf("[WARN] can't save comment, %s", err) - httpError(w, r, http.StatusInternalServerError, err, "can't save comment") + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't save comment") return } @@ -160,7 +161,7 @@ func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { err := s.DataService.Delete(locator, id) if err != nil { log.Printf("[WARN] can't delete comment %s %+v, %s", id, locator, err) - httpError(w, r, http.StatusInternalServerError, err, "can't delete comment") + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't delete comment") return } @@ -185,7 +186,7 @@ func (s *Server) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { comments, err := s.DataService.Find(store.Request{Locator: locator, Sort: r.URL.Query().Get("sort")}) if err != nil { log.Printf("[WARN] can't get comments for %+v, %s", locator, err) - httpError(w, r, http.StatusInternalServerError, err, "can't load comments comment") + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't load comments comment") return } comments = s.mod.maskBlockedUsers(comments) @@ -217,7 +218,7 @@ func (s *Server) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { comments, err := s.DataService.Last(store.Locator{SiteID: r.URL.Query().Get("site")}, max) if err != nil { log.Printf("[WARN] can't get last comments, %s", err) - httpError(w, r, http.StatusInternalServerError, err, "can't get last comments") + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get last comments") return } comments = s.mod.maskBlockedUsers(comments) @@ -238,7 +239,7 @@ func (s *Server) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { comment, err := s.DataService.GetByID(locator, id) if err != nil { log.Printf("[WARN] can't get comment, %s", err) - httpError(w, r, http.StatusInternalServerError, err, "can't get comment by id") + common.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get comment by id") return } render.Status(r, http.StatusOK) @@ -261,7 +262,7 @@ func (s *Server) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { comments, err := s.DataService.GetByUser(store.Locator{SiteID: r.URL.Query().Get("site")}, userID) if err != nil { log.Printf("[WARN] can't get comment, %s", err) - httpError(w, r, http.StatusBadRequest, err, "can't get comment by user id") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by user id") return } @@ -272,9 +273,9 @@ func (s *Server) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { // GET /user - returns user info func (s *Server) userInfoCtrl(w http.ResponseWriter, r *http.Request) { - user, err := auth.GetUserInfo(r) + user, err := common.GetUserInfo(r) if err != nil { - httpError(w, r, http.StatusUnauthorized, err, "can't get user info") + common.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") return } render.JSON(w, r, user) @@ -285,7 +286,7 @@ func (s *Server) countCtrl(w http.ResponseWriter, r *http.Request) { locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} count, err := s.DataService.Count(locator) if err != nil { - httpError(w, r, http.StatusBadRequest, err, "can't get count") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count") return } render.JSON(w, r, JSON{"count": count, "loc": locator}) @@ -296,7 +297,7 @@ func (s *Server) listCtrl(w http.ResponseWriter, r *http.Request) { locator := store.Locator{SiteID: r.URL.Query().Get("site")} posts, err := s.DataService.List(locator) if err != nil { - httpError(w, r, http.StatusBadRequest, err, "can't get count") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count") return } render.JSON(w, r, JSON{"posts": posts, "loc": locator}) @@ -305,9 +306,9 @@ func (s *Server) listCtrl(w http.ResponseWriter, r *http.Request) { // PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) { - user, err := auth.GetUserInfo(r) + user, err := common.GetUserInfo(r) if err != nil { - httpError(w, r, http.StatusUnauthorized, err, "can't get user info") + common.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") return } locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} @@ -319,18 +320,13 @@ func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) { comment, err := s.DataService.Vote(locator, id, user.ID, vote) if err != nil { log.Printf("[WARN] vote rejected for %s - %s, %s", user.ID, id, err) - httpError(w, r, http.StatusBadRequest, err, "can't vote for comment") + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment") return } s.respCache.Flush() render.JSON(w, r, JSON{"id": comment.ID, "score": comment.Score}) } -func httpError(w http.ResponseWriter, r *http.Request, code int, err error, details string) { - render.Status(r, code) - render.JSON(w, r, JSON{"error": err.Error(), "details": details}) -} - // renderJSONWithHTML allows html tags and forces charset=utf-8 func renderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) { buf := &bytes.Buffer{}