Skip to content

Commit

Permalink
web/handlers: revamp error localization
Browse files Browse the repository at this point in the history
fixes #66
  • Loading branch information
cryptix committed Apr 12, 2021
1 parent f9652c6 commit 81bd943
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 135 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ require (
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59
go.cryptoscope.co/netwrap v0.1.1
go.cryptoscope.co/secretstream v1.2.2
go.mindeco.de v1.9.0
go.mindeco.de v1.10.0
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mindeco.de v1.9.0 h1:/xli02DkzpIUZxp/rp1nj8z/OZ9MHvkMIr9TfDVcmBg=
go.mindeco.de v1.9.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de v1.10.0 h1:H/bhL+dIgZZnUgBEDlKUJBisTszNiHDONeGZtGdiJJ0=
go.mindeco.de v1.10.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870/go.mod h1:OnBnV02ux4lLsZ39LID6yYLqSDp+dqTHb/3miYPkQFs=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
Expand Down
25 changes: 17 additions & 8 deletions web/errors/badrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import (
"fmt"
)

type ErrNotFound struct {
What string
}
var (
ErrNotAuthorized = errors.New("rooms/web: not authorized")
ErrDenied = errors.New("rooms: this key has been banned")
)

type ErrNotFound struct{ What string }

func (nf ErrNotFound) Error() string {
return fmt.Sprintf("rooms/web: item not found: %s", nf.What)
Expand All @@ -25,14 +28,20 @@ func (br ErrBadRequest) Error() string {
return fmt.Sprintf("rooms/web: bad request error: %s", br.Details)
}

type ErrForbidden struct {
Details error
}
type ErrForbidden struct{ Details error }

func (f ErrForbidden) Error() string {
return fmt.Sprintf("rooms/web: access denied: %s", f.Details)
}

var ErrNotAuthorized = errors.New("rooms/web: not authorized")
type PageNotFound struct{ Path string }

var ErrDenied = errors.New("rooms: this key has been banned")
func (e PageNotFound) Error() string {
return fmt.Sprintf("rooms/web: page not found: %s", e.Path)
}

type DatabaseError struct{ Reason error }

func (e DatabaseError) Error() string {
return fmt.Sprintf("rooms/web: database failed to complete query: %s", e.Reason.Error())
}
107 changes: 107 additions & 0 deletions web/errors/errhandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package errors

import (
"errors"
"html/template"
"net/http"

"github.com/go-kit/kit/log/level"
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
"go.mindeco.de/logging"

"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
)

type ErrorHandler struct {
locHelper *i18n.Helper
render *render.Renderer
}

func NewErrorHandler(locHelper *i18n.Helper) *ErrorHandler {
return &ErrorHandler{
locHelper: locHelper,
}
}

// SetRenderer needs to update the rendere later since we need to pass ErrorHandler into render.New (ie. befor we get the pointer for r)
func (eh *ErrorHandler) SetRenderer(r *render.Renderer) {
eh.render = r
}

func (eh *ErrorHandler) Handle(rw http.ResponseWriter, req *http.Request, code int, err error) {
var ih = i18n.LocalizerFromRequest(eh.locHelper, req)

// default, unlocalized message
msg := err.Error()

// localize some specific error messages
var (
aa roomdb.ErrAlreadyAdded
pnf PageNotFound
br ErrBadRequest
f ErrForbidden
)

switch {
case err == ErrNotAuthorized:
code = http.StatusForbidden
msg = ih.LocalizeSimple("ErrorAuthBadLogin")

case err == auth.ErrBadLogin:
msg = ih.LocalizeSimple("ErrorAuthBadLogin")

case errors.Is(err, roomdb.ErrNotFound):
code = http.StatusNotFound
msg = ih.LocalizeSimple("ErrorNotFound")

case errors.As(err, &aa):
msg = ih.LocalizeWithData("ErrorAlreadyAdded", map[string]string{
"Feed": aa.Ref.Ref(),
})

case errors.As(err, &pnf):
code = http.StatusNotFound
msg = ih.LocalizeWithData("ErrorPageNotFound", map[string]string{
"Path": pnf.Path,
})

case errors.As(err, &br):
code = http.StatusBadRequest
// TODO: we could localize all the "Where:" as labels, too
// buttt it feels like overkill right now
msg = ih.LocalizeWithData("ErrorBadRequest", map[string]string{
"Where": br.Where,
"Details": br.Details.Error(),
})

case errors.As(err, &f):
code = http.StatusForbidden
msg = ih.LocalizeWithData("ErrorForbidden", map[string]string{
"Details": f.Details.Error(),
})
}

data := errorTemplateData{
Err: template.HTML(msg),
// TODO: localize status codes? might be fine with a few
Status: http.StatusText(code),
StatusCode: code,
}

renderErr := eh.render.Render(rw, req, "error.tmpl", code, data)
if renderErr != nil {
logger := logging.FromContext(req.Context())
level.Error(logger).Log("event", "error template renderfailed",
"orig-err", err,
"render-err", renderErr,
)
}
}

type errorTemplateData struct {
StatusCode int
Status string
Err template.HTML
}
4 changes: 3 additions & 1 deletion web/handlers/admin/aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,13 @@ func (h aliasesHandler) revoke(rw http.ResponseWriter, req *http.Request) {
err = h.db.Revoke(req.Context(), req.FormValue("name"))
if err != nil {
if !errors.Is(err, roomdb.ErrNotFound) {

// TODO: flash error
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
status = http.StatusNotFound
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}

http.Redirect(rw, req, redirectToAliases, status)
Expand Down
21 changes: 9 additions & 12 deletions web/handlers/admin/denied_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
"net/http"
"strconv"

"github.com/gorilla/csrf"
"go.mindeco.de/http/render"
refs "go.mindeco.de/ssb-refs"

"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
refs "go.mindeco.de/ssb-refs"
)

type deniedKeysHandler struct {
Expand All @@ -26,21 +26,21 @@ const redirectToDeniedKeys = "/admin/denied"

func (h deniedKeysHandler) add(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

newEntry := req.Form.Get("pub_key")
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "Public Key", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

Expand All @@ -53,11 +53,7 @@ func (h deniedKeysHandler) add(w http.ResponseWriter, req *http.Request) {
var aa roomdb.ErrAlreadyAdded
if errors.As(err, &aa) {
code = http.StatusBadRequest
// TODO: localized error pages
// h.r.Error(w, req, http.StatusBadRequest, weberrors.Localize())
// return
}

h.r.Error(w, req, code, err)
return
}
Expand Down Expand Up @@ -98,6 +94,7 @@ func (h deniedKeysHandler) removeConfirm(rw http.ResponseWriter, req *http.Reque
entry, err := h.db.GetByID(req.Context(), id)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
// TODO "flash" errors
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusFound)
return nil, ErrRedirected
}
Expand Down
8 changes: 3 additions & 5 deletions web/handlers/admin/invites.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,15 @@ func (h invitesHandler) overview(rw http.ResponseWriter, req *http.Request) (int

func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "POST" {
// TODO: proper error type
return nil, fmt.Errorf("bad request")
return nil, weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
return nil, fmt.Errorf("bad request: %w", err)
return nil, weberrors.ErrBadRequest{Where: "Form data", Details: err}
}

member := members.FromContext(req.Context())
if member == nil {
return nil, fmt.Errorf("warning: no user session for elevated access request")
return nil, weberrors.ErrNotAuthorized
}
pm, err := h.config.GetPrivacyMode(req.Context())
if err != nil {
Expand Down
47 changes: 22 additions & 25 deletions web/handlers/admin/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@ const redirectToMembers = "/admin/members"

func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

if err := req.ParseForm(); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

newEntry := req.Form.Get("pub_key")
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad public key: %w", err))
err = weberrors.ErrBadRequest{Where: "Public Key", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

Expand All @@ -54,11 +54,7 @@ func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
var aa roomdb.ErrAlreadyAdded
if errors.As(err, &aa) {
code = http.StatusBadRequest
// TODO: localized error pages
// h.r.Error(w, req, http.StatusBadRequest, weberrors.Localize())
// return
}

h.r.Error(w, req, code, err)
return
}
Expand All @@ -68,41 +64,42 @@ func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {

func (h membersHandler) changeRole(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

if err := req.ParseForm(); err != nil {
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

currentMember := members.FromContext(req.Context())
if currentMember == nil || currentMember.Role != roomdb.RoleAdmin {
// TODO: proper error type
h.r.Error(w, req, http.StatusForbidden, fmt.Errorf("not an admin"))
err := weberrors.ErrForbidden{Details: fmt.Errorf("not an admin")}
h.r.Error(w, req, http.StatusForbidden, err)
return
}

memberID, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
if err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad member id: %w", err))
return
}

if err := req.ParseForm(); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "id", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

var role roomdb.Role
if err := role.UnmarshalText([]byte(req.Form.Get("role"))); err != nil {
// TODO: proper error type
err = weberrors.ErrBadRequest{Where: "role", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}

if err := h.db.SetRole(req.Context(), memberID, role); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusInternalServerError, fmt.Errorf("failed to change member role: %w", err))
err = weberrors.DatabaseError{Reason: err}
// TODO: not found error
h.r.Error(w, req, http.StatusInternalServerError, err)
return
}

Expand Down
Loading

0 comments on commit 81bd943

Please sign in to comment.