From ab664aafc3efeba7ad62e517c059a6a9ba74fb95 Mon Sep 17 00:00:00 2001 From: boreq Date: Tue, 13 Dec 2022 19:05:45 +0100 Subject: [PATCH] Add a JSON endpoint for generating invites When running in open mode invites can be freely generated by accessing /create-invite. This displays an HTML page which creates and displays an invite to the user. This commit adds an additional way of creating invites in open mode. A POST request can be sent to the same /create-invite endpoint with the Accept header set to application/json. This returns a JSON response which contains an invite url. The purpose of this change is to make automatic invite generation easier in SSB clients. --- web/handlers/http.go | 7 +++++- web/handlers/invites.go | 44 +++++++++++++++++++++++++++++++++++- web/handlers/invites_test.go | 40 ++++++++++++++++++++++++++++++++ web/router/complete.go | 2 +- 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/web/handlers/http.go b/web/handlers/http.go index 5f5577b..b4655a0 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -364,7 +364,7 @@ func New( m.Get(router.CompleteInviteFacadeFallback).Handler(r.HTML("invite/facade-fallback.tmpl", ih.presentFacadeFallback)) m.Get(router.CompleteInviteInsertID).Handler(r.HTML("invite/insert-id.tmpl", ih.presentInsert)) m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume) - m.Get(router.OpenModeCreateInvite).HandlerFunc(r.HTML("admin/invite-created.tmpl", ih.createOpenMode)) + m.Get(router.OpenModeCreateInvite).HandlerFunc(ih.createOpenMode) // static assets m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets))) @@ -379,6 +379,7 @@ func New( mainMux.Handle("/", m) consumeURL := urlTo(router.CompleteInviteConsume) + openModeCreateInviteURL := urlTo(router.OpenModeCreateInvite) // apply HTTP middleware middlewares := []func(http.Handler) http.Handler{ @@ -394,6 +395,10 @@ func New( next.ServeHTTP(w, csrf.UnsafeSkipCheck(req)) return } + if req.URL.Path == openModeCreateInviteURL.Path && req.Header.Get("Accept") == "application/json" { + next.ServeHTTP(w, csrf.UnsafeSkipCheck(req)) + return + } next.ServeHTTP(w, req) }) }, diff --git a/web/handlers/invites.go b/web/handlers/invites.go index edc34ca..49ce5f0 100644 --- a/web/handlers/invites.go +++ b/web/handlers/invites.go @@ -350,7 +350,22 @@ func (html inviteConsumeHTMLResponder) SendError(err error) { html.renderer.Error(html.rw, html.req, http.StatusInternalServerError, err) } -func (h inviteHandler) createOpenMode(rw http.ResponseWriter, req *http.Request) (interface{}, error) { +func (h inviteHandler) createOpenMode(rw http.ResponseWriter, r *http.Request) { + switch r.Header.Get("Accept") { + case "application/json": + if r.Method != http.MethodPost { + h.render.Error(rw, r, http.StatusBadRequest, errors.New("invalid method")) + } + h.createOpenModeJSON(rw, r) + default: + if r.Method != http.MethodGet { + h.render.Error(rw, r, http.StatusBadRequest, errors.New("invalid method")) + } + h.render.HTML("admin/invite-created.tmpl", h.createOpenModeHTML)(rw, r) + } +} + +func (h inviteHandler) createOpenModeHTML(rw http.ResponseWriter, req *http.Request) (interface{}, error) { ctx := req.Context() token, err := h.invites.Create(ctx, -1) @@ -364,3 +379,30 @@ func (h inviteHandler) createOpenMode(rw http.ResponseWriter, req *http.Request) "FacadeURL": facadeURL.String(), }, nil } + +func (h inviteHandler) createOpenModeJSON(rw http.ResponseWriter, req *http.Request) { + logger := logging.FromContext(req.Context()) + ctx := req.Context() + enc := json.NewEncoder(rw) + + token, err := h.invites.Create(ctx, -1) + if err != nil { + data := struct { + Status string `json:"status"` + Error string `json:"error"` + }{"failed", err.Error()} + rw.WriteHeader(http.StatusInternalServerError) + if err := enc.Encode(data); err != nil { + level.Warn(logger).Log("event", "sending json error failed", "err", err) + } + return + } + + response := map[string]string{ + "url": h.urlTo(router.CompleteInviteFacade, "token", token).String(), + } + + if err := enc.Encode(response); err != nil { + level.Warn(logger).Log("event", "sending json response failed", "err", err) + } +} diff --git a/web/handlers/invites_test.go b/web/handlers/invites_test.go index 71d03e5..c6b1cba 100644 --- a/web/handlers/invites_test.go +++ b/web/handlers/invites_test.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/json" "net/http" + "net/http/httptest" "net/url" "strings" "testing" @@ -342,3 +343,42 @@ func TestInviteConsumptionDenied(t *testing.T) { // invite should not be consumed r.EqualValues(0, ts.InvitesDB.ConsumeCallCount()) } + +func TestOpenModeCreateInviteHTML(t *testing.T) { + ts := setup(t) + r := require.New(t) + + someToken := "fake-token" + ts.InvitesDB.CreateReturns(someToken, nil) + + doc, resp := ts.Client.GetHTML(ts.URLTo(router.OpenModeCreateInvite)) + r.Equal(http.StatusOK, resp.Code) + + facadeLink := doc.Find("#invite-facade-link") + r.NotNil(facadeLink) + r.Contains(facadeLink.AttrOr("href", ""), someToken) + r.Contains(facadeLink.Text(), someToken) +} + +func TestOpenModeCreateInviteJSON(t *testing.T) { + ts := setup(t) + r := require.New(t) + + someToken := "fake-token" + ts.InvitesDB.CreateReturns(someToken, nil) + + req, err := http.NewRequest("POST", ts.URLTo(router.OpenModeCreateInvite).String(), nil) + r.NoError(err) + + req.Header.Set("Accept", "application/json") + + recorder := httptest.NewRecorder() + ts.Mux.ServeHTTP(recorder, req) + r.Equal(http.StatusOK, recorder.Code) + + response := map[string]string{} + err = json.Unmarshal(recorder.Body.Bytes(), &response) + r.NoError(err) + + require.Contains(t, response["url"], someToken) +} diff --git a/web/router/complete.go b/web/router/complete.go index 5dcdc7e..3b30f29 100644 --- a/web/router/complete.go +++ b/web/router/complete.go @@ -44,7 +44,7 @@ func CompleteApp() *mux.Router { m.Path("/members/change-password").Methods("GET").Name(MembersChangePasswordForm) m.Path("/members/change-password").Methods("POST").Name(MembersChangePassword) - m.Path("/create-invite").Methods("GET").Name(OpenModeCreateInvite) + m.Path("/create-invite").Methods("GET", "POST").Name(OpenModeCreateInvite) m.Path("/join").Methods("GET").Name(CompleteInviteFacade) m.Path("/join-fallback").Methods("GET").Name(CompleteInviteFacadeFallback) m.Path("/join-manually").Methods("GET").Name(CompleteInviteInsertID)