From 81bd94344e4975ca2d01f45a3e8188c75faa4a8b Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 30 Mar 2021 12:48:42 +0200 Subject: [PATCH] web/handlers: revamp error localization fixes #66 --- go.mod | 2 +- go.sum | 2 + web/errors/badrequest.go | 25 ++++--- web/errors/errhandler.go | 107 ++++++++++++++++++++++++++++++ web/handlers/admin/aliases.go | 4 +- web/handlers/admin/denied_keys.go | 23 +++---- web/handlers/admin/invites.go | 8 +-- web/handlers/admin/members.go | 47 ++++++------- web/handlers/http.go | 100 ++++++++++------------------ web/i18n/defaults/active.en.toml | 86 ++++++++++++++++++------ web/i18n/helper.go | 12 ++++ 11 files changed, 280 insertions(+), 136 deletions(-) create mode 100644 web/errors/errhandler.go diff --git a/go.mod b/go.mod index fc8972f..9d423dc 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 3410d25..5be28ab 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/web/errors/badrequest.go b/web/errors/badrequest.go index bcfc3c7..ff68752 100644 --- a/web/errors/badrequest.go +++ b/web/errors/badrequest.go @@ -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) @@ -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()) +} diff --git a/web/errors/errhandler.go b/web/errors/errhandler.go new file mode 100644 index 0000000..cc76d63 --- /dev/null +++ b/web/errors/errhandler.go @@ -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 +} diff --git a/web/handlers/admin/aliases.go b/web/handlers/admin/aliases.go index 7aab7b1..5342c54 100644 --- a/web/handlers/admin/aliases.go +++ b/web/handlers/admin/aliases.go @@ -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) diff --git a/web/handlers/admin/denied_keys.go b/web/handlers/admin/denied_keys.go index 1877a23..26fbfaf 100644 --- a/web/handlers/admin/denied_keys.go +++ b/web/handlers/admin/denied_keys.go @@ -8,12 +8,12 @@ import ( "net/http" "strconv" - "go.mindeco.de/http/render" - refs "go.mindeco.de/ssb-refs" - "github.com/gorilla/csrf" + "go.mindeco.de/http/render" + "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 { @@ -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 } @@ -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 } @@ -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 } diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go index f465c9f..3bd9343 100644 --- a/web/handlers/admin/invites.go +++ b/web/handlers/admin/invites.go @@ -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 { diff --git a/web/handlers/admin/members.go b/web/handlers/admin/members.go index 0b38af4..3a690e6 100644 --- a/web/handlers/admin/members.go +++ b/web/handlers/admin/members.go @@ -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 } @@ -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 } @@ -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 } diff --git a/web/handlers/http.go b/web/handlers/http.go index 9c7a9b9..5162021 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -3,7 +3,6 @@ package handlers import ( - "errors" "fmt" "html/template" "net/http" @@ -25,6 +24,7 @@ import ( "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/roomstate" "github.com/ssb-ngi-pointer/go-ssb-room/web" + weberrs "github.com/ssb-ngi-pointer/go-ssb-room/web/errors" "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/admin" roomsAuth "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth" "github.com/ssb-ngi-pointer/go-ssb-room/web/i18n" @@ -78,16 +78,23 @@ func New( return nil, err } + eh := weberrs.NewErrorHandler(locHelper) + + allTheTemplates := concatTemplates( + HTMLTemplates, + roomsAuth.HTMLTemplates, + admin.HTMLTemplates, + ) + allTheTemplates = append(allTheTemplates, "error.tmpl") + r, err := render.New(web.Templates, render.SetLogger(logger), render.BaseTemplates("base.tmpl", "menu.tmpl"), - render.AddTemplates(concatTemplates( - HTMLTemplates, - roomsAuth.HTMLTemplates, - admin.HTMLTemplates, - )...), - render.ErrorTemplate("error.tmpl"), + render.AddTemplates(allTheTemplates...), + // render.ErrorTemplate(), + render.SetErrorHandler(eh.Handle), render.FuncMap(web.TemplateFuncs(m)), + // TODO: move these to the i18n helper pkg render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} { loc := i18n.LocalizerFromRequest(locHelper, r) @@ -97,6 +104,7 @@ func New( loc := i18n.LocalizerFromRequest(locHelper, r) return loc.LocalizeSimple }), + render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} { return func(routeName string) bool { route := m.Get(routeName) @@ -110,6 +118,7 @@ func New( return r.RequestURI == url.Path } }), + render.InjectTemplateFunc("urlToNotice", func(r *http.Request) interface{} { return func(name string) *url.URL { noticeName := roomdb.PinnedNoticeName(name) @@ -135,11 +144,13 @@ func New( return u } }), + render.InjectTemplateFunc("is_logged_in", members.TemplateHelper()), ) if err != nil { return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err) } + eh.SetRenderer(r) cookieCodec, err := web.LoadOrCreateCookieSecrets(repo) if err != nil { @@ -154,52 +165,14 @@ func New( }, } - // TODO: this is just the error handler for http/auth, not render - authErrH := func(rw http.ResponseWriter, req *http.Request, err error, code int) { - var ih = i18n.LocalizerFromRequest(locHelper, req) - - // default, unlocalized message - msg := err.Error() - - // localize some specific error messages - var ( - aa roomdb.ErrAlreadyAdded - ) - switch { - case err == auth.ErrBadLogin: - msg = ih.LocalizeSimple("AuthErrorBadLogin") - - case errors.Is(err, roomdb.ErrNotFound): - msg = ih.LocalizeSimple("ErrorNotFound") - - case errors.As(err, &aa): - msg = ih.LocalizeSimple("ErrorAlreadyAdded") - } - - r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) { - return errorTemplateData{ - Err: msg, - // TODO: localize? - Status: http.StatusText(code), - StatusCode: code, - }, nil - }).ServeHTTP(rw, req) - } - - notAuthorizedH := r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) { - statusCode := http.StatusUnauthorized - rw.WriteHeader(statusCode) - return errorTemplateData{ - statusCode, - "Unauthorized", - "you are not authorized to access the requested site", - }, nil - }) - authWithPassword, err := auth.NewHandler(dbs.AuthFallback, auth.SetStore(cookieStore), - auth.SetErrorHandler(authErrH), - auth.SetNotAuthorizedHandler(notAuthorizedH), + auth.SetErrorHandler(func(rw http.ResponseWriter, req *http.Request, err error, code int) { + eh.Handle(rw, req, code, err) + }), + auth.SetNotAuthorizedHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + eh.Handle(rw, req, http.StatusForbidden, weberrs.ErrNotAuthorized) + })), auth.SetLifetime(2*time.Hour), // TODO: configure ) if err != nil { @@ -237,6 +210,7 @@ func New( bridge, ) + // auth routes m.Get(router.AuthLogin).HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if label := req.URL.Query().Get("ssb-http-auth"); label != "" { authWithSSB.DecideMethod(w, req) @@ -246,13 +220,11 @@ func New( }) m.Get(router.AuthFallbackFinalize).HandlerFunc(authWithPassword.Authorize) - m.Get(router.AuthFallbackLogin).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) { return map[string]interface{}{ csrf.TemplateTag: csrf.TemplateField(req), }, nil })) - m.Get(router.AuthLogout).HandlerFunc(func(w http.ResponseWriter, req *http.Request) { err = authWithSSB.Logout(w, req) if err != nil { @@ -261,6 +233,7 @@ func New( authWithPassword.Logout(w, req) }) + // all the admin routes adminHandler := admin.Handler( netInfo.Domain, r, @@ -277,7 +250,10 @@ func New( ) mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler)) + // landing page m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) { + // TODO: try websocket upgrade (issue #) + notice, err := dbs.PinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB") if err != nil { return nil, fmt.Errorf("failed to find description: %w", err) @@ -292,6 +268,7 @@ func New( })) m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl")) + // notices (the mini-CMS) var nh = noticeHandler{ notices: dbs.Notices, pinned: dbs.PinnedNotices, @@ -299,6 +276,7 @@ func New( m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nh.list)) m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nh.show)) + // public aliases var ah = aliasHandler{ r: r, @@ -309,6 +287,7 @@ func New( } m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve) + //public invites var ih = inviteHandler{ render: r, @@ -324,14 +303,14 @@ func New( m.Get(router.CompleteInviteInsertID).Handler(r.HTML("invite/insert-id.tmpl", ih.presentInsert)) m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume) + // statuc assets m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets))) - m.NotFoundHandler = r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) { - rw.WriteHeader(http.StatusNotFound) - msg := i18n.LocalizerFromRequest(locHelper, req).LocalizeSimple("PageNotFound") - return errorTemplateData{http.StatusNotFound, "Not Found", msg}, nil + m.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + eh.Handle(rw, req, http.StatusNotFound, weberrs.PageNotFound{Path: req.URL.Path}) }) + // hook up main stdlib mux to the gorrilla/mux with named routes mainMux.Handle("/", m) urlTo := web.NewURLTo(m) @@ -370,13 +349,6 @@ func New( } // utils - -type errorTemplateData struct { - StatusCode int - Status string - Err string -} - func concatTemplates(lst ...[]string) []string { var catted []string diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index 905fa47..be535ec 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -1,3 +1,6 @@ +# default localiztion file for english + +# generic terms GenericConfirm = "Yes" GenericGoBack = "Back" GenericSave = "Save" @@ -11,32 +14,77 @@ PageNotFound = "The requested page was not found." PubKeyRefPlaceholder = "@ .ed25519" +# roles RoleMember = "Member" RoleModerator = "Moderator" RoleAdmin = "Admin" +# navigation labels (should be single words or as short as possible) +NavAdminLanding = "Home" +NavAdminDashboard = "Dashboard" +NavAdminInvites = "Invites" +NavAdminNotices = "Notices" + +# Error messages +ErrorAuthBadLogin = "The supplied authentication credentials seem wrong." +ErrorNotFound = "The database couldn't find the item in question." +ErrorAlreadyAdded = "The public key {{.Key}} already is on the list" +ErrorPageNotFound = "The requested page ({{.Path}}) is not there." +ErrorNotAuthorized = "You are not authorized to access this page." +ErrorForbidden = "The request could not be executed because of lacking privileges ({{.Details}})" +ErrorBadRequest = "There was a problem with your Request: {{.Where}} ({{.Details}}" + +# TODO: might be obsolete with notices LandingTitle = "ohai my room" LandingWelcome = "Landing welcome here" -AuthSignIn = "Sign in" -AuthSignOut = "Sign out" +# authentication +################ AuthTitle = "Member Authentication" AuthWelcome = "If you are a member of this room, you can access the internal dashboard. Click on your preferred sign-in method below:" +AuthSignIn = "Sign in" +AuthSignOut = "Sign out" + +# auth with ssb AuthWithSSBTitle = "Sign in with SSB" AuthWithSSBInstruct = "Easy and secure method, if your SSB app supports it." AuthWithSSBWelcome = "To sign-in with your SSB identity stored on this device, press the button below which will open a compatible SSB app, if it's installed." AuthWithSSBInstructQR = "If your SSB app is on another device, you can scan the following QR code to sign-in with that device's SSB identity." AuthWithSSBError = "Sign-in failed. Please make sure you use an SSB app that supports this method of login, and click the button above within a minute after this page was opened." +# auth with password AuthFallbackTitle = "Password sign-in" AuthFallbackWelcome = "Signing in with username and password is only possible if the administrator has given you one, because we do not support user registration." AuthFallbackInstruct = "This method is an acceptable fallback, if you have a username and password." +# general dashboard stuff +######################### + AdminDashboardTitle = "Dashboard" AdminDashboardWelcome = "Welcome to your dashboard" +# privacy modes +############### + +ModeOpen = "Open" +ModeCommunity = "Community" +ModeRestricted = "Restricted" + +SetPrivacyModeTitle = "Set Privacy Mode" +PrivacyModesTitle = "Privacy Modes" +RoomsSpecification = "rooms 2 specification" +ExplanationPrivacyModes = "The privacy mode of this room determines who can create invites and who can connect to the room. For more information, see the" +ExplanationOpen = "Open invite codes, anyone may connect" +ExplanationCommunity = "Members can create invites, anyone may connect" +ExplanationRestricted = "Only admins/mods can create invites, only members may connect" + +Settings = "Settings" + +# banned dashboard +################## + AdminDeniedKeysTitle = "Banned" AdminDeniedKeysWelcome = "This page can be used to ban SSB IDs so that they can't access the room any more." AdminDeniedKeysAdd = "Add" @@ -46,6 +94,9 @@ AdminDeniedKeysCommentDescription = "The person who added this ban, added the fo AdminDeniedKeysRemoveConfirmWelcome = "Are you sure you want to remove this ban? They will will be able to access the room again." AdminDeniedKeysRemoveConfirmTitle = "Confirm member removal" +# members dashboard +################### + AdminMembersTitle = "Members" AdminMembersWelcome = "Here you can see all the members of the room and ways to add new ones (by their SSB ID) or remove exising ones." AdminMembersAdd = "Add" @@ -61,6 +112,9 @@ AdminMemberDetailsAliasRevoke = "Revoke" AdminMemberDetailsExclusion = "Exclusion from this room" AdminMemberDetailsRemove = "Remove member" +# invite dashboard +################## + AdminInvitesTitle = "Invites" AdminInvitesWelcome = "Create invite tokens for people who are not yet members of this room. On this page you can also see previously created invites that are still not unclaimed by new members." AdminInvitesCreate = "Create new invite" @@ -80,10 +134,8 @@ AdminInviteSuggestedAliasIsShort = "Alias:" AdminInviteCreatedTitle = "Invite created successfully!" AdminInviteCreatedInstruct = "Now, copy the link below and paste it to a friend who you want to invite to this room." -NavAdminLanding = "Home" -NavAdminDashboard = "Dashboard" -NavAdminInvites = "Invites" -NavAdminNotices = "Notices" +# public invites +################ InviteFacade = "Join Room" InviteFacadeTitle = "Join Room" @@ -101,6 +153,9 @@ InviteInsertWelcome = "You can claim your invite by inserting your SSB ID below. InviteConsumedTitle = "Invite accepted!" InviteConsumedWelcome = "You are now a member of this room. If you need a multiserver address to connect to the room, you can copy-paste the one below:" +# notices (mini-CMS) +#################### + NoticeEditTitle = "Edit Notice" NoticeList = "Notices" NoticeListWelcome = "Here you can manage the contents of the landing page and other important documents such as code of conduct and privacy policy." @@ -111,19 +166,12 @@ NoticeNews = "News" NoticeDescription = "Description" NoticePrivacyPolicy = "Privacy Policy" -ModeOpen = "Open" -ModeCommunity = "Community" -ModeRestricted = "Restricted" - -SetPrivacyModeTitle = "Set Privacy Mode" -PrivacyModesTitle = "Privacy Modes" -RoomsSpecification = "rooms 2 specification" -ExplanationPrivacyModes = "The privacy mode of this room determines who can create invites and who can connect to the room. For more information, see the" -ExplanationOpen = "Open invite codes, anyone may connect" -ExplanationCommunity = "Members can create invites, anyone may connect" -ExplanationRestricted = "Only admins/mods can create invites, only members may connect" - -Settings = "Settings" +# Plurals +######### +# These need to use this form and get {{.Count}} +# [Label] +# one = "singular" +# other = "{{.Count}} things" [MemberCount] description = "Number of members" diff --git a/web/i18n/helper.go b/web/i18n/helper.go index 2084192..f4e71f1 100644 --- a/web/i18n/helper.go +++ b/web/i18n/helper.go @@ -139,6 +139,18 @@ func (l Localizer) LocalizeSimple(messageID string) string { panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err)) } +func (l Localizer) LocalizeWithData(messageID string, tplData map[string]string) string { + msg, err := l.loc.Localize(&i18n.LocalizeConfig{ + MessageID: messageID, + TemplateData: tplData, + }) + if err == nil { + return msg + } + + panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err)) +} + func (l Localizer) LocalizePlurals(messageID string, pluralCount int) string { msg, err := l.loc.Localize(&i18n.LocalizeConfig{ MessageID: messageID,