From d1d108b9b7756705ce8c6b8bfec47beaffe11592 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 26 Mar 2021 20:08:13 +0100 Subject: [PATCH] Add support for invite consumption via JSON --- cmd/server/main.go | 4 +- internal/network/interface.go | 2 + roomdb/sqlite/invites.go | 2 +- roomdb/sqlite/invites_test.go | 4 + web/handlers/admin/invites.go | 9 +- web/handlers/admin/invites_test.go | 4 +- web/handlers/auth/withssb.go | 1 + web/handlers/auth_test.go | 3 +- web/handlers/http.go | 37 ++- web/handlers/invites.go | 233 +++++++++++++++--- web/handlers/invites_test.go | 120 ++++++--- web/i18n/defaults/active.en.toml | 10 +- web/router/complete.go | 4 +- web/templates/admin/invite-created.tmpl | 8 +- .../admin/invite-revoke-confirm.tmpl | 2 +- web/templates/invite/accept.tmpl | 48 ---- web/templates/invite/consumed.tmpl | 3 +- web/templates/invite/facade.tmpl | 39 +++ 18 files changed, 380 insertions(+), 153 deletions(-) delete mode 100644 web/templates/invite/accept.tmpl create mode 100644 web/templates/invite/facade.tmpl diff --git a/cmd/server/main.go b/cmd/server/main.go index 10e6703..d0945bd 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -131,7 +131,7 @@ func runroomsrv() error { if !development { return fmt.Errorf("https-domain can't be empty. See '%s -h' for a full list of options", os.Args[0]) } - httpsDomain = "dev.testing.local" + httpsDomain = "localhost" } // validate listen addresses to bail out on invalid flag input before doing anything else @@ -254,6 +254,8 @@ func runroomsrv() error { PortHTTPS: uint(portHTTP), PortMUXRPC: uint(portMUXRPC), RoomID: roomsrv.Whoami(), + + Development: development, }, roomsrv.StateManager, roomsrv.Network, diff --git a/internal/network/interface.go b/internal/network/interface.go index d89355f..490ba11 100644 --- a/internal/network/interface.go +++ b/internal/network/interface.go @@ -27,6 +27,8 @@ type ServerEndpointDetails struct { RoomID refs.FeedRef Domain string + + Development bool } // MultiserverAddress returns net:domain:muxport~shs:roomPubKeyInBase64 diff --git a/roomdb/sqlite/invites.go b/roomdb/sqlite/invites.go index 261623a..6df6410 100644 --- a/roomdb/sqlite/invites.go +++ b/roomdb/sqlite/invites.go @@ -162,7 +162,7 @@ func (i Invites) GetByToken(ctx context.Context, token string) (roomdb.Invite, e } entry, err := models.Invites( - qm.Where("active = true AND token = ?", ht), + qm.Where("active = true AND hashed_token = ?", ht), qm.Load("CreatedByMember"), ).One(ctx, i.db) if err != nil { diff --git a/roomdb/sqlite/invites_test.go b/roomdb/sqlite/invites_test.go index 3a29a3d..d4adfc0 100644 --- a/roomdb/sqlite/invites_test.go +++ b/roomdb/sqlite/invites_test.go @@ -79,6 +79,10 @@ func TestInvites(t *testing.T) { _, nope := db.Members.GetByFeed(ctx, newMember) r.Error(nope, "expected feed to not yet be on the allow list") + gotInv, err := db.Invites.GetByToken(ctx, tok) + r.NoError(err) + r.Equal(lst[0].ID, gotInv.ID) + inv, err := db.Invites.Consume(ctx, tok, newMember) r.NoError(err, "failed to consume the invite") r.Equal(testMemberNick, inv.CreatedBy.Nickname) diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go index 12ad9d6..21a53a4 100644 --- a/web/handlers/admin/invites.go +++ b/web/handlers/admin/invites.go @@ -67,13 +67,12 @@ func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interf } urlTo := web.NewURLTo(router.CompleteApp()) - acceptURL := urlTo(router.CompleteInviteAccept, "token", token) - acceptURL.Host = h.domainName - acceptURL.Scheme = "https" + facadeURL := urlTo(router.CompleteInviteFacade, "token", token) + facadeURL.Host = h.domainName + facadeURL.Scheme = "https" return map[string]interface{}{ - "Token": token, - "AccepURL": acceptURL.String(), + "FacadeURL": facadeURL.String(), "AliasSuggestion": aliasSuggestion, }, nil diff --git a/web/handlers/admin/invites_test.go b/web/handlers/admin/invites_test.go index 3eeb452..7547ddf 100644 --- a/web/handlers/admin/invites_test.go +++ b/web/handlers/admin/invites_test.go @@ -128,10 +128,10 @@ func TestInvitesCreate(t *testing.T) { {"#welcome", "AdminInviteCreatedWelcome"}, }) - wantURL := urlTo(router.CompleteInviteAccept, "token", testInvite) + wantURL := urlTo(router.CompleteInviteFacade, "token", testInvite) wantURL.Host = ts.Domain wantURL.Scheme = "https" - shownLink := doc.Find("#invite-accept-link").Text() + shownLink := doc.Find("#invite-facade-link").Text() a.Equal(wantURL.String(), shownLink) } diff --git a/web/handlers/auth/withssb.go b/web/handlers/auth/withssb.go index 5f1c409..e3a0278 100644 --- a/web/handlers/auth/withssb.go +++ b/web/handlers/auth/withssb.go @@ -34,6 +34,7 @@ import ( var HTMLTemplates = []string{ "auth/decide_method.tmpl", + "auth/fallback_sign_in.tmpl", "auth/withssb_server_start.tmpl", } diff --git a/web/handlers/auth_test.go b/web/handlers/auth_test.go index e845b21..dbdc24c 100644 --- a/web/handlers/auth_test.go +++ b/web/handlers/auth_test.go @@ -431,8 +431,7 @@ func TestAuthWithSSBServerInitHappyPath(t *testing.T) { a.Equal("start-http-auth", qry.Get("action")) a.Equal(serverChallenge, qry.Get("sc")) a.Equal(ts.NetworkInfo.RoomID.Ref(), qry.Get("sid")) - var msaddr = fmt.Sprintf("net:%s:%d~shs:%s", ts.NetworkInfo.Domain, ts.NetworkInfo.PortMUXRPC, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())) - a.Equal(msaddr, qry.Get("multiserverAddress")) + a.Equal(ts.NetworkInfo.MultiserverAddress(), qry.Get("multiserverAddress")) qrCode, has := html.Find("#start-auth-qrcode").Attr("src") a.True(has, "should have the inline image data") diff --git a/web/handlers/http.go b/web/handlers/http.go index 0ea0b1c..39ea77d 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -36,11 +36,13 @@ var HTMLTemplates = []string{ "landing/index.tmpl", "landing/about.tmpl", "aliases-resolved.html", - "invite/accept.tmpl", + "invite/consumed.tmpl", - "auth/fallback_sign_in.tmpl", + "invite/facade.tmpl", + "notice/list.tmpl", "notice/show.tmpl", + "error.tmpl", } @@ -94,7 +96,7 @@ func New( }), render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} { return func(routeName string) bool { - route := router.CompleteApp().Get(routeName) + route := m.Get(routeName) if route == nil { return false } @@ -115,7 +117,7 @@ func New( if err != nil { return nil } - route := router.CompleteApp().GetRoute(router.CompleteNoticeShow) + route := m.GetRoute(router.CompleteNoticeShow) if route == nil { return nil } @@ -220,8 +222,6 @@ func New( mainMux := &http.ServeMux{} // start hooking up handlers to the router - var muxrpcHostAndPort = fmt.Sprintf("%s:%d", netInfo.Domain, netInfo.PortMUXRPC) - authWithSSB := roomsAuth.NewWithSSBHandler( m, r, @@ -299,13 +299,14 @@ func New( m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve) var ih = inviteHandler{ + render: r, + invites: dbs.Invites, - roomPubKey: netInfo.RoomID.PubKey(), - muxrpcHostAndPort: muxrpcHostAndPort, + networkInfo: netInfo, } - m.Get(router.CompleteInviteAccept).Handler(r.HTML("invite/accept.tmpl", ih.acceptForm)) - m.Get(router.CompleteInviteConsume).Handler(r.HTML("invite/consumed.tmpl", ih.consume)) + m.Get(router.CompleteInviteFacade).Handler(r.HTML("invite/facade.tmpl", ih.presentFacade)) + m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume) m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets))) @@ -317,11 +318,27 @@ func New( mainMux.Handle("/", m) + urlTo := web.NewURLTo(m) + consumeURL := urlTo(router.CompleteInviteConsume) + // apply HTTP middleware middlewares := []func(http.Handler) http.Handler{ logging.InjectHandler(logger), members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB), CSRF, + + // We disable CSRF for certain requests that are done by apps + // only if they already contain some secret (like invite consumption) + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ct := req.Header.Get("Content-Type") + if req.URL.Path == consumeURL.Path && ct == "application/json" { + next.ServeHTTP(w, csrf.UnsafeSkipCheck(req)) + return + } + next.ServeHTTP(w, req) + }) + }, } if !web.Production { diff --git a/web/handlers/invites.go b/web/handlers/invites.go index 93fec4b..4218ea3 100644 --- a/web/handlers/invites.go +++ b/web/handlers/invites.go @@ -1,30 +1,35 @@ package handlers import ( - "encoding/base64" + "encoding/json" "errors" "fmt" + "html/template" "net/http" - - "go.mindeco.de/logging" - "golang.org/x/crypto/ed25519" + "net/url" "github.com/go-kit/kit/log/level" "github.com/gorilla/csrf" + "go.mindeco.de/http/render" + "go.mindeco.de/logging" + + "github.com/ssb-ngi-pointer/go-ssb-room/internal/network" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + "github.com/ssb-ngi-pointer/go-ssb-room/web" weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors" + "github.com/ssb-ngi-pointer/go-ssb-room/web/router" refs "go.mindeco.de/ssb-refs" ) type inviteHandler struct { - invites roomdb.InvitesService - aliases roomdb.AliasesService + render *render.Renderer - muxrpcHostAndPort string - roomPubKey ed25519.PublicKey + invites roomdb.InvitesService + + networkInfo network.ServerEndpointDetails } -func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (interface{}, error) { +func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request) (interface{}, error) { token := req.URL.Query().Get("token") inv, err := h.invites.GetByToken(req.Context(), token) @@ -35,49 +40,201 @@ func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (in return nil, err } - return map[string]interface{}{ - "Token": token, - "Invite": inv, + var joinRoomURI url.URL + joinRoomURI.Scheme = "ssb" + joinRoomURI.Opaque = "experimental" + queryVals := make(url.Values) + queryVals.Set("action", "join-room") + queryVals.Set("invite", token) + + urlTo := web.NewURLTo(router.CompleteApp()) + submissionURL := urlTo(router.CompleteInviteConsume) + submissionURL.Host = h.networkInfo.Domain + submissionURL.Scheme = "https" + if h.networkInfo.Development { + submissionURL.Scheme = "http" + submissionURL.Host += fmt.Sprintf(":%d", h.networkInfo.PortHTTPS) + } + queryVals.Set("postTo", submissionURL.String()) + + joinRoomURI.RawQuery = queryVals.Encode() + + return map[string]interface{}{ csrf.TemplateTag: csrf.TemplateField(req), + + "Invite": inv, + "Token": token, + + "JoinRoomURI": template.URL(joinRoomURI.String()), }, nil } -func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (interface{}, error) { - if err := req.ParseForm(); err != nil { - return nil, weberrors.ErrBadRequest{Where: "form data", Details: err} +type inviteConsumePayload struct { + ID refs.FeedRef `json:"id"` + Invite string `json:"invite"` +} + +func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) { + logger := logging.FromContext(req.Context()) + + var ( + token string + newMember refs.FeedRef + + resp inviteConsumeResponder + ) + ct := req.Header.Get("Content-Type") + switch ct { + case "application/json": + resp = newinviteConsumeJSONResponder(rw) + + var body inviteConsumePayload + + level.Debug(logger).Log("event", "handling json body") + err := json.NewDecoder(req.Body).Decode(&body) + if err != nil { + err = fmt.Errorf("consume body contained invalid json: %w", err) + resp.SendError(err) + return + } + + newMember = body.ID + token = body.Invite + + case "application/x-www-form-urlencoded": + resp = newinviteConsumeHTMLResponder(h.render, rw, req) + + if err := req.ParseForm(); err != nil { + err = weberrors.ErrBadRequest{Where: "form data", Details: err} + resp.SendError(err) + return + } + + token = req.FormValue("invite") + + parsedID, err := refs.ParseFeedRef(req.FormValue("id")) + if err != nil { + err = weberrors.ErrBadRequest{Where: "id", Details: err} + resp.SendError(err) + return + } + newMember = *parsedID + + default: + http.Error(rw, fmt.Sprintf("unhandled Content-Type (%q)", ct), http.StatusBadRequest) + return } + resp.UpdateMultiserverAddr(h.networkInfo.MultiserverAddress()) - alias := req.FormValue("alias") - token := req.FormValue("token") - - newMember, err := refs.ParseFeedRef(req.FormValue("new_member")) - if err != nil { - return nil, weberrors.ErrBadRequest{Where: "new_member", Details: err} - } - - inv, err := h.invites.Consume(req.Context(), token, *newMember) + inv, err := h.invites.Consume(req.Context(), token, newMember) if err != nil { if errors.Is(err, roomdb.ErrNotFound) { - return nil, weberrors.ErrNotFound{What: "invite"} + resp.SendError(weberrors.ErrNotFound{What: "invite"}) + return } - return nil, err + resp.SendError(err) + return } log := logging.FromContext(req.Context()) level.Info(log).Log("event", "invite consumed", "id", inv.ID, "ref", newMember.ShortRef()) - if alias != "" { - level.Warn(log).Log( - "TODO", "invite registration", - "alias", alias, - ) + resp.SendSuccess() +} + +// inviteConsumeResponder is supposed to handle different encoding types transparently. +// It either sends the rooms multiaddress on success or an error. +type inviteConsumeResponder interface { + SendSuccess() + SendError(error) + + UpdateMultiserverAddr(string) +} + +// inviteConsumeJSONResponse dictates the field names and format of the JSON response for the inviteConsume web endpoint +type inviteConsumeJSONResponse struct { + Status string `json:"status"` + + RoomAddress string `json:"multiserverAddress"` +} + +// handles JSON responses +type inviteConsumeJSONResponder struct { + enc *json.Encoder + + multiservAddr string +} + +func newinviteConsumeJSONResponder(rw http.ResponseWriter) inviteConsumeResponder { + rw.Header().Set("Content-Type", "application/json") + return &inviteConsumeJSONResponder{ + enc: json.NewEncoder(rw), + } +} + +func (json *inviteConsumeJSONResponder) UpdateMultiserverAddr(msaddr string) { + json.multiservAddr = msaddr +} + +func (json inviteConsumeJSONResponder) SendSuccess() { + var resp = inviteConsumeJSONResponse{ + Status: "successful", + RoomAddress: json.multiservAddr, + } + json.enc.Encode(resp) +} + +func (json inviteConsumeJSONResponder) SendError(err error) { + json.enc.Encode(struct { + Status string `json:"status"` + Error string `json:"error"` + }{"error", err.Error()}) +} + +// handles HTML responses +type inviteConsumeHTMLResponder struct { + renderer *render.Renderer + rw http.ResponseWriter + req *http.Request + + multiservAddr string +} + +func newinviteConsumeHTMLResponder(r *render.Renderer, rw http.ResponseWriter, req *http.Request) inviteConsumeResponder { + return &inviteConsumeHTMLResponder{ + renderer: r, + rw: rw, + req: req, + } +} + +func (html *inviteConsumeHTMLResponder) UpdateMultiserverAddr(msaddr string) { + html.multiservAddr = msaddr +} + +func (html inviteConsumeHTMLResponder) SendSuccess() { + + // construct the ssb:experimental?action=consume-invite&... uri for linking into apps + queryParams := url.Values{} + queryParams.Set("action", "join-room") + queryParams.Set("multiserverAddress", html.multiservAddr) + + // html.multiservAddr + ssbURI := url.URL{ + Scheme: "ssb", + Opaque: "experimental", + RawQuery: queryParams.Encode(), } - // TODO: hardcoded here just to be replaced soon with next version of ssb-uri - roomPubKey := base64.StdEncoding.EncodeToString(h.roomPubKey) - roomAddr := fmt.Sprintf("net:%s~shs:%s:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", h.muxrpcHostAndPort, roomPubKey) - - return map[string]interface{}{ - "RoomAddress": roomAddr, - }, nil + err := html.renderer.Render(html.rw, html.req, "invite/consumed.tmpl", http.StatusOK, struct { + SSBURI template.URL + }{template.URL(ssbURI.String())}) + if err != nil { + logger := logging.FromContext(html.req.Context()) + level.Warn(logger).Log("event", "render failed", "err", err) + } +} + +func (html inviteConsumeHTMLResponder) SendError(err error) { + html.renderer.Error(html.rw, html.req, http.StatusInternalServerError, err) } diff --git a/web/handlers/invites_test.go b/web/handlers/invites_test.go index a66e5bc..248099d 100644 --- a/web/handlers/invites_test.go +++ b/web/handlers/invites_test.go @@ -3,6 +3,7 @@ package handlers import ( "bytes" "encoding/base64" + "encoding/json" "net/http" "net/http/cookiejar" "net/url" @@ -10,16 +11,15 @@ import ( "testing" "github.com/PuerkitoBio/goquery" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - refs "go.mindeco.de/ssb-refs" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/web" weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors" "github.com/ssb-ngi-pointer/go-ssb-room/web/router" "github.com/ssb-ngi-pointer/go-ssb-room/web/webassert" + refs "go.mindeco.de/ssb-refs" ) func TestInviteShowAcceptForm(t *testing.T) { @@ -31,7 +31,7 @@ func TestInviteShowAcceptForm(t *testing.T) { a, r := assert.New(t), require.New(t) testToken := "nonexistant-test-token" - acceptURL404 := urlTo(router.CompleteInviteAccept, "token", testToken) + acceptURL404 := urlTo(router.CompleteInviteFacade, "token", testToken) r.NotNil(acceptURL404) // prep the mocked db for http:404 @@ -65,7 +65,7 @@ func TestInviteShowAcceptForm(t *testing.T) { a, r := assert.New(t), require.New(t) testToken := "existing-test-token" - validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken) + validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken) r.NotNil(validAcceptURL) // prep the mocked db for http:200 @@ -87,8 +87,8 @@ func TestInviteShowAcceptForm(t *testing.T) { a.Equal(testToken, tokenFromArg) webassert.Localized(t, doc, []webassert.LocalizedElement{ - {"#welcome", "InviteAcceptWelcome"}, - {"title", "InviteAcceptTitle"}, + {"#welcome", "InviteFacadeWelcome"}, + {"title", "InviteFacadeTitle"}, }) form := doc.Find("form#consume") @@ -97,9 +97,8 @@ func TestInviteShowAcceptForm(t *testing.T) { webassert.CSRFTokenPresent(t, form) webassert.ElementsInForm(t, form, []webassert.FormElement{ - {Name: "token", Type: "hidden", Value: testToken}, - {Name: "alias", Type: "text", Value: fakeExistingInvite.AliasSuggestion}, - {Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder}, + {Name: "invite", Type: "hidden", Value: testToken}, + {Name: "id", Type: "text", Placeholder: wantNewMemberPlaceholder}, }) }) @@ -107,11 +106,11 @@ func TestInviteShowAcceptForm(t *testing.T) { a, r := assert.New(t), require.New(t) testToken := "existing-test-token-2" - validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken) + validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken) r.NotNil(validAcceptURL) - inviteWithNoAlias := roomdb.Invite{ID: 4321} - ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil) + testInvite := roomdb.Invite{ID: 4321} + ts.InvitesDB.GetByTokenReturns(testInvite, nil) // request the form validAcceptForm := validAcceptURL.String() @@ -125,8 +124,8 @@ func TestInviteShowAcceptForm(t *testing.T) { a.Equal(testToken, tokenFromArg) webassert.Localized(t, doc, []webassert.LocalizedElement{ - {"#welcome", "InviteAcceptWelcome"}, - {"title", "InviteAcceptTitle"}, + {"#welcome", "InviteFacadeWelcome"}, + {"title", "InviteFacadeTitle"}, }) form := doc.Find("form#consume") @@ -134,26 +133,25 @@ func TestInviteShowAcceptForm(t *testing.T) { webassert.CSRFTokenPresent(t, form) webassert.ElementsInForm(t, form, []webassert.FormElement{ - {Name: "token", Type: "hidden", Value: testToken}, - {Name: "alias", Type: "text", Placeholder: "you@this.room"}, - {Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder}, + {Name: "invite", Type: "hidden", Value: testToken}, + {Name: "id", Type: "text", Placeholder: wantNewMemberPlaceholder}, }) }) } -func TestInviteConsumeInvite(t *testing.T) { +func TestInviteConsumeInviteHTTP(t *testing.T) { ts := setup(t) a, r := assert.New(t), require.New(t) urlTo := web.NewURLTo(ts.Router) testToken := "existing-test-token-2" - validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken) + validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken) r.NotNil(validAcceptURL) validAcceptURL.Host = "localhost" validAcceptURL.Scheme = "https" - inviteWithNoAlias := roomdb.Invite{ID: 4321} - ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil) + testInvite := roomdb.Invite{ID: 4321} + ts.InvitesDB.GetByTokenReturns(testInvite, nil) // request the form (for a valid csrf token) validAcceptForm := validAcceptURL.String() @@ -184,8 +182,8 @@ func TestInviteConsumeInvite(t *testing.T) { Algo: refs.RefAlgoFeedSSB1, } consumeVals := url.Values{ - "token": []string{testToken}, - "new_member": []string{testNewMember.Ref()}, + "invite": []string{testToken}, + "id": []string{testNewMember.Ref()}, csrfName: []string{csrfValue}, } @@ -207,7 +205,7 @@ func TestInviteConsumeInvite(t *testing.T) { ts.Client.SetHeaders(csrfCookieHeader) // prepare the mock - ts.InvitesDB.ConsumeReturns(inviteWithNoAlias, nil) + ts.InvitesDB.ConsumeReturns(testInvite, nil) // send the POST resp = ts.Client.PostForm(consumeInviteURL.String(), consumeVals) @@ -222,11 +220,75 @@ func TestInviteConsumeInvite(t *testing.T) { consumedDoc, err := goquery.NewDocumentFromReader(resp.Body) r.NoError(err) - gotRA := consumedDoc.Find("#room-address").Text() + joinHref, ok := consumedDoc.Find("#join-link").Attr("href") + a.True(ok) + + // validate ssb-uri + joinURI, err := url.Parse(joinHref) + r.NoError(err) + + a.Equal("ssb", joinURI.Scheme) + a.Equal("experimental", joinURI.Opaque) + + params := joinURI.Query() + a.Equal("join-room", params.Get("action")) + + gotRA := params.Get("multiserverAddress") - // TODO: this is just a cheap stub for actual ssb-uri parsing a.True(strings.HasPrefix(gotRA, "net:localhost:8008~shs:"), "not for the test host: %s", gotRA) - a.True(strings.Contains(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key missing? %s", gotRA) - a.True(strings.HasSuffix(gotRA, ":SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24="), "magic suffix missing: %s", gotRA) - + a.True(strings.HasSuffix(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key missing? %s", gotRA) +} + +func TestInviteConsumeInviteJSON(t *testing.T) { + ts := setup(t) + a, r := assert.New(t), require.New(t) + urlTo := web.NewURLTo(ts.Router) + + testToken := "existing-test-token-2" + validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken) + r.NotNil(validAcceptURL) + validAcceptURL.Host = "localhost" + validAcceptURL.Scheme = "https" + + testInvite := roomdb.Invite{ID: 4321} + ts.InvitesDB.GetByTokenReturns(testInvite, nil) + + // create the consume request + testNewMember := refs.FeedRef{ + ID: bytes.Repeat([]byte{1}, 32), + Algo: refs.RefAlgoFeedSSB1, + } + + var consume inviteConsumePayload + consume.Invite = testToken + consume.ID = testNewMember + + // construct the consume endpoint url + consumeInviteURL, err := ts.Router.Get(router.CompleteInviteConsume).URL() + r.Nil(err) + consumeInviteURL.Host = "localhost" + consumeInviteURL.Scheme = "https" + + // prepare the mock + ts.InvitesDB.ConsumeReturns(testInvite, nil) + + // send the POST + resp := ts.Client.SendJSON(consumeInviteURL.String(), consume) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for sign in") + + // check how consume was called + r.EqualValues(1, ts.InvitesDB.ConsumeCallCount()) + _, tokenFromArg, newMemberRef := ts.InvitesDB.ConsumeArgsForCall(0) + a.Equal(testToken, tokenFromArg) + a.True(newMemberRef.Equal(&testNewMember)) + + var jsonConsumeResp inviteConsumeJSONResponse + err = json.NewDecoder(resp.Body).Decode(&jsonConsumeResp) + r.NoError(err) + + a.Equal("successful", jsonConsumeResp.Status) + + gotRA := jsonConsumeResp.RoomAddress + a.True(strings.HasPrefix(gotRA, "net:localhost:8008~shs:"), "not for the test host: %s", gotRA) + a.True(strings.HasSuffix(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key missing? %s", gotRA) } diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index 76badc1..0f39fa2 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -81,11 +81,11 @@ NavAdminDashboard = "Dashboard" NavAdminInvites = "Invites" NavAdminNotices = "Notices" -InviteAccept = "Accept invite" -InviteAcceptTitle = "Accept Invite" -InviteAcceptWelcome = "elaborate welcome message for a new member with good words and stuff." -InviteAcceptAliasSuggestion = "The persone who created thought you might like this alias:" -InviteAcceptPublicKey = "Public Key" +InviteFacade = "Join Room" +InviteFacadeTitle = "Join Room" +InviteFacadeWelcome = "elaborate welcome message for a new member with good words and stuff." +InviteFacadeAliasSuggestion = "The persone who created thought you might like this alias:" +InviteFacadePublicKey = "Public Key" InviteConsumedTitle = "Invite accepted!" InviteConsumedWelcome = "Even more elaborate message that the person is now a member of the room!" diff --git a/web/router/complete.go b/web/router/complete.go index eaa838e..43c4717 100644 --- a/web/router/complete.go +++ b/web/router/complete.go @@ -16,7 +16,7 @@ const ( CompleteAliasResolve = "complete:alias:resolve" - CompleteInviteAccept = "complete:invite:accept" + CompleteInviteFacade = "complete:invite:accept" CompleteInviteConsume = "complete:invite:consume" ) @@ -32,7 +32,7 @@ func CompleteApp() *mux.Router { m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve) - m.Path("/invite/accept").Methods("GET").Name(CompleteInviteAccept) + m.Path("/join").Methods("GET").Name(CompleteInviteFacade) m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume) m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow) diff --git a/web/templates/admin/invite-created.tmpl b/web/templates/admin/invite-created.tmpl index 2dc2c7f..571b4ab 100644 --- a/web/templates/admin/invite-created.tmpl +++ b/web/templates/admin/invite-created.tmpl @@ -7,13 +7,7 @@ class="text-center" >{{i18n "AdminInviteCreatedWelcome"}} - {{i18n "InviteAccept"}} - - + {{if ne .AliasSuggestion ""}} diff --git a/web/templates/admin/invite-revoke-confirm.tmpl b/web/templates/admin/invite-revoke-confirm.tmpl index 1cf688d..81765ab 100644 --- a/web/templates/admin/invite-revoke-confirm.tmpl +++ b/web/templates/admin/invite-revoke-confirm.tmpl @@ -9,7 +9,7 @@
{{.Invite.CreatedBy.Name}}
+ >{{.Invite.CreatedBy.Nickname}} {{if ne .Invite.AliasSuggestion ""}} diff --git a/web/templates/invite/accept.tmpl b/web/templates/invite/accept.tmpl deleted file mode 100644 index 7b3fec3..0000000 --- a/web/templates/invite/accept.tmpl +++ /dev/null @@ -1,48 +0,0 @@ -{{ define "title" }}{{ i18n "InviteAcceptTitle" }}{{ end }} -{{ define "content" }} -
- - {{ i18n "InviteAcceptWelcome" }} - -
- {{ .csrfField }} - -
- -
- - - TODO: make this a dropdown -
- -

{{ i18n "InviteAcceptAliasSuggestion" }}

- - - {{i18n "GenericGoBack"}} - - -
-
-
-{{ end }} \ No newline at end of file diff --git a/web/templates/invite/consumed.tmpl b/web/templates/invite/consumed.tmpl index 240b4e2..af93cd6 100644 --- a/web/templates/invite/consumed.tmpl +++ b/web/templates/invite/consumed.tmpl @@ -7,7 +7,6 @@ class="text-center" >{{i18n "InviteConsumedWelcome"}} -

TODO: this is just a room v1 invite. present tunnel address and ssb uri redirect

-
{{.RoomAddress}}
+ Join Room {{end}} \ No newline at end of file diff --git a/web/templates/invite/facade.tmpl b/web/templates/invite/facade.tmpl new file mode 100644 index 0000000..df5a3aa --- /dev/null +++ b/web/templates/invite/facade.tmpl @@ -0,0 +1,39 @@ +{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }} +{{ define "content" }} +
+ + {{ i18n "InviteFacadeWelcome" }} + + Join + +
+

TODO: html form fallback / advanced use

+ +
+ {{ .csrfField }} + + + +
+ + +
+ + +
+ +
+{{ end }} \ No newline at end of file