From c7bcef4339c59ee2472fad9fc4edf242d603a633 Mon Sep 17 00:00:00 2001
From: Henry
Date: Wed, 3 Mar 2021 13:58:06 +0100
Subject: [PATCH 01/12] invite form testing
---
cmd/server/main.go | 1 +
web/handlers/admin/allow_list.go | 2 +
web/handlers/admin/allow_list_test.go | 1 +
web/handlers/admin/app_test.go | 10 ++-
web/handlers/admin/handler.go | 10 +++
web/handlers/admin/invites.go | 84 +++++++++++++++++++++++++
web/handlers/admin/invites_test.go | 47 ++++++++++++++
web/handlers/http.go | 8 ++-
web/handlers/http_test.go | 12 ++++
web/i18n/defaults/active.en.toml | 6 ++
web/router/admin.go | 6 ++
web/templates/admin/invites.tmpl | 89 +++++++++++++++++++++++++++
web/templates/menu.tmpl | 9 +++
13 files changed, 283 insertions(+), 2 deletions(-)
create mode 100644 web/handlers/admin/invites.go
create mode 100644 web/handlers/admin/invites_test.go
create mode 100644 web/templates/admin/invites.tmpl
diff --git a/cmd/server/main.go b/cmd/server/main.go
index bdb6a71f..58a6184c 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -217,6 +217,7 @@ func runroomsrv() error {
db.AuthWithSSB,
db.AuthFallback,
db.AllowList,
+ db.Invites,
db.Notices,
db.PinnedNotices,
)
diff --git a/web/handlers/admin/allow_list.go b/web/handlers/admin/allow_list.go
index dc231e7c..9ffb6f64 100644
--- a/web/handlers/admin/allow_list.go
+++ b/web/handlers/admin/allow_list.go
@@ -76,6 +76,8 @@ func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interfa
}
count := len(lst)
+ // TODO: generalize paginator code
+
num, err := strconv.ParseInt(req.URL.Query().Get("page"), 10, 32)
if err != nil {
num = 1
diff --git a/web/handlers/admin/allow_list_test.go b/web/handlers/admin/allow_list_test.go
index ade86b4f..d825acb6 100644
--- a/web/handlers/admin/allow_list_test.go
+++ b/web/handlers/admin/allow_list_test.go
@@ -64,6 +64,7 @@ func TestAllowListAdd(t *testing.T) {
a.EqualValues(1, inputSelection.Length())
name, ok := inputSelection.Attr("name")
+ a.True(ok, "field has a name")
a.Equal("pub_key", name, "wrong name on input field")
newKey := "@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519"
diff --git a/web/handlers/admin/app_test.go b/web/handlers/admin/app_test.go
index ccbd9e99..6c36aa98 100644
--- a/web/handlers/admin/app_test.go
+++ b/web/handlers/admin/app_test.go
@@ -28,6 +28,7 @@ type testSession struct {
AllowListDB *mockdb.FakeAllowListService
PinnedDB *mockdb.FakePinnedNoticesService
NoticeDB *mockdb.FakeNoticesService
+ InvitesDB *mockdb.FakeInviteService
RoomState *roomstate.Manager
}
@@ -39,6 +40,7 @@ func newSession(t *testing.T) *testSession {
ts.AllowListDB = new(mockdb.FakeAllowListService)
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
ts.NoticeDB = new(mockdb.FakeNoticesService)
+ ts.InvitesDB = new(mockdb.FakeInviteService)
log, _ := logtest.KitLogger("admin", t)
ctx := context.TODO()
@@ -75,7 +77,13 @@ func newSession(t *testing.T) *testSession {
}
ts.Mux = http.NewServeMux()
- ts.Mux.Handle("/", Handler(r, ts.RoomState, ts.AllowListDB, ts.NoticeDB, ts.PinnedDB))
+ ts.Mux.Handle("/", Handler(r,
+ ts.RoomState,
+ ts.AllowListDB,
+ ts.InvitesDB,
+ ts.NoticeDB,
+ ts.PinnedDB,
+ ))
ts.Client = tester.New(ts.Mux, t)
return &ts
diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go
index f9a34be7..12a6d52d 100644
--- a/web/handlers/admin/handler.go
+++ b/web/handlers/admin/handler.go
@@ -21,6 +21,8 @@ var HTMLTemplates = []string{
"admin/allow-list.tmpl",
"admin/allow-list-remove-confirm.tmpl",
+ "admin/invites.tmpl",
+
"admin/notice-edit.tmpl",
}
@@ -30,6 +32,7 @@ func Handler(
r *render.Renderer,
roomState *roomstate.Manager,
al admindb.AllowListService,
+ is admindb.InviteService,
ndb admindb.NoticesService,
pdb admindb.PinnedNoticesService,
) http.Handler {
@@ -57,6 +60,13 @@ func Handler(
mux.HandleFunc("/members/remove/confirm", r.HTML("admin/allow-list-remove-confirm.tmpl", ah.removeConfirm))
mux.HandleFunc("/members/remove", ah.remove)
+ var ih = invitesH{
+ r: r,
+ db: is,
+ }
+
+ mux.HandleFunc("/invites", r.HTML("admin/invites.tmpl", ih.overview))
+
var nh = noticeHandler{
r: r,
noticeDB: ndb,
diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go
new file mode 100644
index 00000000..9bf29b62
--- /dev/null
+++ b/web/handlers/admin/invites.go
@@ -0,0 +1,84 @@
+package admin
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "go.mindeco.de/http/render"
+
+ "github.com/gorilla/csrf"
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "github.com/vcraescu/go-paginator/v2"
+ "github.com/vcraescu/go-paginator/v2/adapter"
+ "github.com/vcraescu/go-paginator/v2/view"
+)
+
+type invitesH struct {
+ r *render.Renderer
+
+ db admindb.InviteService
+}
+
+func (h invitesH) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+ lst, err := h.db.List(req.Context())
+ if err != nil {
+ return nil, err
+ }
+
+ // Reverse the slice to provide recent-to-oldest results
+ for i, j := 0, len(lst)-1; i < j; i, j = i+1, j-1 {
+ lst[i], lst[j] = lst[j], lst[i]
+ }
+
+ // TODO: generalize paginator code
+
+ count := len(lst)
+
+ num, err := strconv.ParseInt(req.URL.Query().Get("page"), 10, 32)
+ if err != nil {
+ num = 1
+ }
+ page := int(num)
+ if page < 1 {
+ page = 1
+ }
+
+ paginator := paginator.New(adapter.NewSliceAdapter(lst), pageSize)
+ paginator.SetPage(page)
+
+ var entries admindb.ListEntries
+ if err = paginator.Results(&entries); err != nil {
+ return nil, fmt.Errorf("paginator failed with %w", err)
+ }
+
+ view := view.New(paginator)
+ pagesSlice, err := view.Pages()
+ if err != nil {
+ return nil, fmt.Errorf("paginator view.Pages failed with %w", err)
+ }
+ if len(pagesSlice) == 0 {
+ pagesSlice = []int{1}
+ }
+ last, err := view.Last()
+ if err != nil {
+ return nil, fmt.Errorf("paginator view.Last failed with %w", err)
+ }
+ firstInView := pagesSlice[0] == 1
+ lastInView := false
+ for _, num := range pagesSlice {
+ if num == last {
+ lastInView = true
+ }
+ }
+
+ return map[string]interface{}{
+ csrf.TemplateTag: csrf.TemplateField(req),
+ "Entries": entries,
+ "Count": count,
+ "Paginator": paginator,
+ "View": view,
+ "FirstInView": firstInView,
+ "LastInView": lastInView,
+ }, nil
+}
diff --git a/web/handlers/admin/invites_test.go b/web/handlers/admin/invites_test.go
new file mode 100644
index 00000000..3816c6fc
--- /dev/null
+++ b/web/handlers/admin/invites_test.go
@@ -0,0 +1,47 @@
+package admin
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/web/router"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestInvitesCreateForm(t *testing.T) {
+ ts := newSession(t)
+ a := assert.New(t)
+
+ url, err := ts.Router.Get(router.AdminInvitesOverview).URL()
+ a.Nil(err)
+
+ html, resp := ts.Client.GetHTML(url.String())
+ a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
+
+ assertLocalized(t, html, []localizedElement{
+ {"#welcome", "AdminInvitesWelcome"},
+ {"title", "AdminInvitesTitle"},
+ })
+
+ formSelection := html.Find("form#create-invite")
+ a.EqualValues(1, formSelection.Length())
+
+ method, ok := formSelection.Attr("method")
+ a.True(ok, "form has method set")
+ a.Equal("POST", method)
+
+ action, ok := formSelection.Attr("action")
+ a.True(ok, "form has action set")
+
+ addURL, err := ts.Router.Get(router.AdminInvitesCreate).URL()
+ a.NoError(err)
+
+ a.Equal(addURL.String(), action)
+
+ inputSelection := formSelection.Find("input[type=text]")
+ a.EqualValues(1, inputSelection.Length())
+
+ name, ok := inputSelection.Attr("name")
+ a.True(ok, "input has a name")
+ a.Equal("alias_suggestion", name, "wrong name on input field")
+}
diff --git a/web/handlers/http.go b/web/handlers/http.go
index 4f88bb5f..9dccb5be 100644
--- a/web/handlers/http.go
+++ b/web/handlers/http.go
@@ -44,6 +44,7 @@ func New(
as admindb.AuthWithSSBService,
fs admindb.AuthFallbackService,
al admindb.AllowListService,
+ is admindb.InviteService,
ns admindb.NoticesService,
ps admindb.PinnedNoticesService,
) (http.Handler, error) {
@@ -221,7 +222,12 @@ func New(
// hookup handlers to the router
roomsAuth.Handler(m, r, a)
- adminHandler := a.Authenticate(admin.Handler(r, roomState, al, ns, ps))
+ adminHandler := a.Authenticate(admin.Handler(r,
+ roomState,
+ al,
+ is,
+ ns,
+ ps))
mainMux.Handle("/admin/", adminHandler)
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
diff --git a/web/handlers/http_test.go b/web/handlers/http_test.go
index 071878f0..aa7c54fc 100644
--- a/web/handlers/http_test.go
+++ b/web/handlers/http_test.go
@@ -113,6 +113,8 @@ func TestFallbackAuth(t *testing.T) {
doc, resp := ts.Client.GetHTML(signInFormURL.String())
a.Equal(http.StatusOK, resp.Code)
+ assertCSRFTokenPresent(t, doc.Find("form"))
+
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 1, "should have one cookie for CSRF protection validation")
t.Log(csrfCookie)
@@ -233,3 +235,13 @@ func assertLocalized(t *testing.T, html *goquery.Document, elems []localizedElem
a.Equal(pair.Label, html.Find(pair.Selector).Text(), "localized pair %d failed", i+1)
}
}
+
+func assertCSRFTokenPresent(t *testing.T, sel *goquery.Selection) {
+ a := assert.New(t)
+
+ csrfField := sel.Find("input[name=gorilla.csrf.Token]")
+ a.EqualValues(1, csrfField.Length(), "no csrf-token input tag")
+ tipe, ok := csrfField.Attr("type")
+ a.True(ok, "csrf input has a type")
+ a.Equal("hidden", tipe, "wrong type on csrf field")
+}
diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml
index 064dde63..cd77251b 100644
--- a/web/i18n/defaults/active.en.toml
+++ b/web/i18n/defaults/active.en.toml
@@ -23,8 +23,14 @@ AdminAllowListRemove = "Remove"
AdminAllowListRemoveConfirmWelcome = "Are you sure you want to remove this member? They will lose their alias, if they have one."
AdminAllowListRemoveConfirmTitle = "Confirm member removal"
+AdminInvitesTitle = "Invites"
+AdminInvitesWelcome = "Here ytou can create invite tokens for people who are not yet members of this room."
+AdminInvitesAliasSuggestion = "Suggested alias (optional)"
+AdminInvitesRevoke = "Revoke"
+
NavAdminLanding = "Home"
NavAdminDashboard = "Dashboard"
+NavAdminInvites = "Invites"
NavAdminNotices = "Notices"
NoticeEditTitle = "Edit Notice"
diff --git a/web/router/admin.go b/web/router/admin.go
index ed980964..09175394 100644
--- a/web/router/admin.go
+++ b/web/router/admin.go
@@ -14,6 +14,9 @@ const (
AdminAllowListRemoveConfirm = "admin:allow-list:remove:confirm"
AdminAllowListRemove = "admin:allow-list:remove"
+ AdminInvitesOverview = "admin:invites:overview"
+ AdminInvitesCreate = "admin:invites:create"
+
AdminNoticeEdit = "admin:notice:edit"
AdminNoticeSave = "admin:notice:save"
AdminNoticeDraftTranslation = "admin:notice:translation:draft"
@@ -39,5 +42,8 @@ func Admin(m *mux.Router) *mux.Router {
m.Path("/notice/translation/add").Methods("POST").Name(AdminNoticeAddTranslation)
m.Path("/notice/save").Methods("POST").Name(AdminNoticeSave)
+ m.Path("/invites").Methods("GET").Name(AdminInvitesOverview)
+ m.Path("/invites/create").Methods("POST").Name(AdminInvitesCreate)
+
return m
}
diff --git a/web/templates/admin/invites.tmpl b/web/templates/admin/invites.tmpl
new file mode 100644
index 00000000..85ee711a
--- /dev/null
+++ b/web/templates/admin/invites.tmpl
@@ -0,0 +1,89 @@
+{{ define "title" }}{{i18n "AdminInvitesTitle"}}{{ end }}
+{{ define "content" }}
+ {{i18n "AdminInvitesTitle"}}
+
+ {{i18n "AdminInvitesWelcome"}}
+
+ {{i18npl "ListCount" .Count}}
+
+
+ {{range .Entries}}
+
+ {{.CreatedBy.Name}}
+
+ {{i18n "AdminInvitesRevoke"}}
+
+ {{end}}
+
+
+ {{$pageNums := .Paginator.PageNums}}
+ {{$view := .View}}
+ {{if gt $pageNums 1}}
+
+ {{if not .FirstInView}}
+
1
+
..
+ {{end}}
+
+ {{range $view.Pages}}
+ {{if le . $pageNums}}
+ {{if eq . $view.Current}}
+
{{.}}
+ {{else}}
+
{{.}}
+ {{end}}
+ {{end}}
+ {{end}}
+
+ {{if not .LastInView}}
+
..
+
{{$view.Last}}
+ {{end}}
+
+ {{end}}
+{{end}}
\ No newline at end of file
diff --git a/web/templates/menu.tmpl b/web/templates/menu.tmpl
index 9f2b6791..3936431f 100644
--- a/web/templates/menu.tmpl
+++ b/web/templates/menu.tmpl
@@ -27,6 +27,15 @@
{{i18n "AdminAllowListTitle"}}
+
+
+
+ {{i18n "NavAdminInvites"}}
+
+
Date: Thu, 4 Mar 2021 10:32:39 +0100
Subject: [PATCH 02/12] re-use pagination code
---
web/handlers/admin/allow_list.go | 54 +++-------------------------
web/handlers/admin/handler.go | 62 ++++++++++++++++++++++++++++++++
web/handlers/admin/invites.go | 55 +++-------------------------
web/handlers/http.go | 4 +--
4 files changed, 71 insertions(+), 104 deletions(-)
diff --git a/web/handlers/admin/allow_list.go b/web/handlers/admin/allow_list.go
index 9ffb6f64..55d251ff 100644
--- a/web/handlers/admin/allow_list.go
+++ b/web/handlers/admin/allow_list.go
@@ -12,9 +12,6 @@ import (
"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
- "github.com/vcraescu/go-paginator/v2"
- "github.com/vcraescu/go-paginator/v2/adapter"
- "github.com/vcraescu/go-paginator/v2/view"
)
type allowListH struct {
@@ -63,8 +60,6 @@ func (h allowListH) add(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, redirectTo, http.StatusFound)
}
-const pageSize = 20
-
func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
lst, err := h.al.List(req.Context())
if err != nil {
@@ -74,56 +69,15 @@ func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interfa
for i, j := 0, len(lst)-1; i < j; i, j = i+1, j-1 {
lst[i], lst[j] = lst[j], lst[i]
}
- count := len(lst)
-
- // TODO: generalize paginator code
- num, err := strconv.ParseInt(req.URL.Query().Get("page"), 10, 32)
+ pageData, err := paginate(lst, len(lst), req.URL.Query())
if err != nil {
- num = 1
- }
- page := int(num)
- if page < 1 {
- page = 1
+ return nil, err
}
- paginator := paginator.New(adapter.NewSliceAdapter(lst), pageSize)
- paginator.SetPage(page)
+ pageData[csrf.TemplateTag] = csrf.TemplateField(req)
- var entries admindb.ListEntries
- if err = paginator.Results(&entries); err != nil {
- return nil, fmt.Errorf("paginator failed with %w", err)
- }
-
- view := view.New(paginator)
- pagesSlice, err := view.Pages()
- if err != nil {
- return nil, fmt.Errorf("paginator view.Pages failed with %w", err)
- }
- if len(pagesSlice) == 0 {
- pagesSlice = []int{1}
- }
- last, err := view.Last()
- if err != nil {
- return nil, fmt.Errorf("paginator view.Last failed with %w", err)
- }
- firstInView := pagesSlice[0] == 1
- lastInView := false
- for _, num := range pagesSlice {
- if num == last {
- lastInView = true
- }
- }
-
- return map[string]interface{}{
- csrf.TemplateTag: csrf.TemplateField(req),
- "Entries": entries,
- "Count": count,
- "Paginator": paginator,
- "View": view,
- "FirstInView": firstInView,
- "LastInView": lastInView,
- }, nil
+ return pageData, nil
}
// TODO: move to render package so that we can decide to not render a page during the controller
diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go
index 12a6d52d..defcd851 100644
--- a/web/handlers/admin/handler.go
+++ b/web/handlers/admin/handler.go
@@ -3,11 +3,16 @@
package admin
import (
+ "fmt"
"net/http"
"net/url"
+ "strconv"
"strings"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "github.com/vcraescu/go-paginator/v2"
+ "github.com/vcraescu/go-paginator/v2/adapter"
+ "github.com/vcraescu/go-paginator/v2/view"
"go.mindeco.de/http/render"
@@ -80,6 +85,63 @@ func Handler(
return customStripPrefix("/admin", mux)
}
+// how many elements does a paginated page have by default
+const defaultPageSize = 20
+
+// paginate receives the total slice and it's length/count, a URL query for the 'limit' and which 'page'.
+//
+// The members of the map are:
+// Entries: the paginated slice
+// Count: the total number of the whole, unpaginated list
+// FirstInView: a bool thats true if you render the first page
+// LastInView: a bool thats true if you render the last page
+// Paginator and View: helpers for rendering the page accessor (see github.com/vcraescu/go-paginator)
+//
+// TODO: we could return a struct instead but then need to re-think how we embedd it into all the pages where we need it.
+// Maybe renderData["Pages"] = paginatedData
+func paginate(total interface{}, count int, qry url.Values) (map[string]interface{}, error) {
+ pageSize, err := strconv.Atoi(qry.Get("limit"))
+ if err != nil {
+ pageSize = defaultPageSize
+ }
+
+ page, err := strconv.Atoi(qry.Get("page"))
+ if err != nil || page < 1 {
+ page = 1
+ }
+
+ paginator := paginator.New(adapter.NewSliceAdapter(total), pageSize)
+ paginator.SetPage(page)
+
+ var entries []interface{}
+ if err = paginator.Results(&entries); err != nil {
+ return nil, fmt.Errorf("paginator failed with %w", err)
+ }
+
+ view := view.New(paginator)
+ pagesSlice, err := view.Pages()
+ if err != nil {
+ return nil, fmt.Errorf("paginator view.Pages failed with %w", err)
+ }
+ if len(pagesSlice) == 0 {
+ pagesSlice = []int{1}
+ }
+
+ last, err := view.Last()
+ if err != nil {
+ return nil, fmt.Errorf("paginator view.Last failed with %w", err)
+ }
+
+ return map[string]interface{}{
+ "Entries": entries,
+ "Count": count,
+ "Paginator": paginator,
+ "View": view,
+ "FirstInView": pagesSlice[0] == 1,
+ "LastInView": pagesSlice[len(pagesSlice)-1] == last,
+ }, nil
+}
+
// trim prefix if exists (workaround for named router problem)
func customStripPrefix(prefix string, h http.Handler) http.Handler {
if prefix == "" {
diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go
index 9bf29b62..8d141653 100644
--- a/web/handlers/admin/invites.go
+++ b/web/handlers/admin/invites.go
@@ -1,17 +1,12 @@
package admin
import (
- "fmt"
"net/http"
- "strconv"
"go.mindeco.de/http/render"
"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
- "github.com/vcraescu/go-paginator/v2"
- "github.com/vcraescu/go-paginator/v2/adapter"
- "github.com/vcraescu/go-paginator/v2/view"
)
type invitesH struct {
@@ -31,54 +26,12 @@ func (h invitesH) overview(rw http.ResponseWriter, req *http.Request) (interface
lst[i], lst[j] = lst[j], lst[i]
}
- // TODO: generalize paginator code
-
- count := len(lst)
-
- num, err := strconv.ParseInt(req.URL.Query().Get("page"), 10, 32)
+ pageData, err := paginate(lst, len(lst), req.URL.Query())
if err != nil {
- num = 1
- }
- page := int(num)
- if page < 1 {
- page = 1
- }
-
- paginator := paginator.New(adapter.NewSliceAdapter(lst), pageSize)
- paginator.SetPage(page)
-
- var entries admindb.ListEntries
- if err = paginator.Results(&entries); err != nil {
- return nil, fmt.Errorf("paginator failed with %w", err)
+ return nil, err
}
- view := view.New(paginator)
- pagesSlice, err := view.Pages()
- if err != nil {
- return nil, fmt.Errorf("paginator view.Pages failed with %w", err)
- }
- if len(pagesSlice) == 0 {
- pagesSlice = []int{1}
- }
- last, err := view.Last()
- if err != nil {
- return nil, fmt.Errorf("paginator view.Last failed with %w", err)
- }
- firstInView := pagesSlice[0] == 1
- lastInView := false
- for _, num := range pagesSlice {
- if num == last {
- lastInView = true
- }
- }
+ pageData[csrf.TemplateTag] = csrf.TemplateField(req)
- return map[string]interface{}{
- csrf.TemplateTag: csrf.TemplateField(req),
- "Entries": entries,
- "Count": count,
- "Paginator": paginator,
- "View": view,
- "FirstInView": firstInView,
- "LastInView": lastInView,
- }, nil
+ return pageData, nil
}
diff --git a/web/handlers/http.go b/web/handlers/http.go
index 9dccb5be..aac6066b 100644
--- a/web/handlers/http.go
+++ b/web/handlers/http.go
@@ -55,8 +55,6 @@ func New(
return nil, err
}
- var a *auth.Handler
-
r, err := render.New(web.Templates,
render.SetLogger(logger),
render.BaseTemplates("base.tmpl", "menu.tmpl"),
@@ -191,7 +189,7 @@ func New(
}, nil
})
- a, err = auth.NewHandler(fs,
+ a, err := auth.NewHandler(fs,
auth.SetStore(store),
auth.SetErrorHandler(authErrH),
auth.SetNotAuthorizedHandler(notAuthorizedH),
From 3c58a1361c6125f24bedbf42f84e7a7c01f53134 Mon Sep 17 00:00:00 2001
From: Henry
Date: Thu, 4 Mar 2021 11:12:37 +0100
Subject: [PATCH 03/12] add endpoint for create
---
web/handlers/admin/handler.go | 3 +--
web/handlers/admin/invites.go | 28 ++++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go
index defcd851..3549e293 100644
--- a/web/handlers/admin/handler.go
+++ b/web/handlers/admin/handler.go
@@ -59,7 +59,6 @@ func Handler(
r: r,
al: al,
}
-
mux.HandleFunc("/members", r.HTML("admin/allow-list.tmpl", ah.overview))
mux.HandleFunc("/members/add", ah.add)
mux.HandleFunc("/members/remove/confirm", r.HTML("admin/allow-list-remove-confirm.tmpl", ah.removeConfirm))
@@ -69,8 +68,8 @@ func Handler(
r: r,
db: is,
}
-
mux.HandleFunc("/invites", r.HTML("admin/invites.tmpl", ih.overview))
+ mux.HandleFunc("/invites/create", ih.create)
var nh = noticeHandler{
r: r,
diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go
index 8d141653..45d473dd 100644
--- a/web/handlers/admin/invites.go
+++ b/web/handlers/admin/invites.go
@@ -1,12 +1,14 @@
package admin
import (
+ "fmt"
"net/http"
"go.mindeco.de/http/render"
"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "github.com/ssb-ngi-pointer/go-ssb-room/web/user"
)
type invitesH struct {
@@ -35,3 +37,29 @@ func (h invitesH) overview(rw http.ResponseWriter, req *http.Request) (interface
return pageData, nil
}
+
+func (h invitesH) create(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"))
+ return
+ }
+ if err := req.ParseForm(); err != nil {
+ // TODO: proper error type
+ h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
+ return
+ }
+
+
+ aliasSuggestion := req.Form.Get("alias_suggestion")
+
+ token, err := h.db.Create(req.Context(), user.ID, aliasSuggestion)
+ if err != nil {
+ h.r.Error(w, req, http.StatusInternalServerError, err)
+ return
+ }
+
+ fmt.Println("use me:", token)
+
+ http.Redirect(w, req, "/admin/invites", http.StatusFound)
+}
From 91dd6017e0533c1cd16f9ebefd9592a908095081 Mon Sep 17 00:00:00 2001
From: Henry
Date: Thu, 4 Mar 2021 11:14:59 +0100
Subject: [PATCH 04/12] add user.FromContext middleware
A helper package so that handler and render code isnt directly tied to
the authentication package.
Also reduces db lookup overhead to one request to sqlite per request for
the user lookup.
---
web/handlers/admin/invites.go | 6 +++
web/handlers/http.go | 44 ++++++++--------------
web/user/helper.go | 71 +++++++++++++++++++++++++++++++++++
3 files changed, 93 insertions(+), 28 deletions(-)
create mode 100644 web/user/helper.go
diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go
index 45d473dd..710cd856 100644
--- a/web/handlers/admin/invites.go
+++ b/web/handlers/admin/invites.go
@@ -50,6 +50,12 @@ func (h invitesH) create(w http.ResponseWriter, req *http.Request) {
return
}
+ user := user.FromContext(req.Context())
+ if user == nil {
+ err := fmt.Errorf("warning: no user session for elevated access request")
+ h.r.Error(w, req, http.StatusInternalServerError, err)
+ return
+ }
aliasSuggestion := req.Form.Get("alias_suggestion")
diff --git a/web/handlers/http.go b/web/handlers/http.go
index aac6066b..7cde53ec 100644
--- a/web/handlers/http.go
+++ b/web/handlers/http.go
@@ -26,6 +26,7 @@ import (
roomsAuth "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
+ "github.com/ssb-ngi-pointer/go-ssb-room/web/user"
)
var HTMLTemplates = []string{
@@ -108,27 +109,7 @@ func New(
return u
}
}),
- render.InjectTemplateFunc("is_logged_in", func(r *http.Request) interface{} {
- no := func() *admindb.User { return nil }
-
- v, err := a.AuthenticateRequest(r)
- if err != nil {
- return no
- }
-
- uid, ok := v.(int64)
- if !ok {
- panic(fmt.Sprintf("warning: not the expected ID type from authenticated session: %T\n", v))
- }
-
- user, err := fs.GetByID(r.Context(), uid)
- if err != nil {
- return no
- }
-
- yes := func() *admindb.User { return user }
- return yes
- }),
+ render.InjectTemplateFunc("is_logged_in", user.TemplateHelper()),
)
if err != nil {
return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err)
@@ -259,16 +240,23 @@ func New(
mainMux.Handle("/", m)
- // apply middleware
- var finalHandler http.Handler = mainMux
- finalHandler = logging.InjectHandler(logger)(finalHandler)
- finalHandler = CSRF(finalHandler)
+ // apply HTTP middleware
+ middlewares := []func(http.Handler) http.Handler{
+ logging.InjectHandler(logger),
+ user.ContextInjecter(fs, a),
+ CSRF,
+ }
- if web.Production {
- return finalHandler, nil
+ if !web.Production {
+ middlewares = append(middlewares, r.GetReloader())
+ }
+
+ var finalHandler http.Handler = mainMux
+ for _, mw := range middlewares {
+ finalHandler = mw(finalHandler)
}
- return r.GetReloader()(finalHandler), nil
+ return finalHandler, nil
}
// utils
diff --git a/web/user/helper.go b/web/user/helper.go
new file mode 100644
index 00000000..33ce7997
--- /dev/null
+++ b/web/user/helper.go
@@ -0,0 +1,71 @@
+// Package user implements helpers for accessing the currently logged in admin or moderator of an active request.
+package user
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "go.mindeco.de/http/auth"
+)
+
+type roomUserContextKeyType string
+
+var roomUserContextKey roomUserContextKeyType = "ssb:room:httpcontext:user"
+
+// FromContext returns the user or nil of it's not logged in
+func FromContext(ctx context.Context) *admindb.User {
+ v := ctx.Value(roomUserContextKey)
+
+ user, ok := v.(*admindb.User)
+ if !ok {
+ return nil
+ }
+
+ return user
+}
+
+// ContextInjecter returns the middleware that injects the user value into the request context
+func ContextInjecter(fs admindb.AuthFallbackService, a *auth.Handler) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+
+ v, err := a.AuthenticateRequest(req)
+ if err != nil {
+ next.ServeHTTP(w, req)
+ return
+ }
+
+ uid, ok := v.(int64)
+ if !ok {
+ next.ServeHTTP(w, req)
+ return
+ }
+
+ user, err := fs.GetByID(req.Context(), uid)
+ if err != nil {
+ next.ServeHTTP(w, req)
+ return
+ }
+
+ ctx := context.WithValue(req.Context(), roomUserContextKey, user)
+ next.ServeHTTP(w, req.WithContext(ctx))
+ })
+ }
+}
+
+// TemplateHelper returns a function to be used with the http/render package.
+// It has to return a function twice because the first is evaluated with the request before it gets passed onto html/template's FuncMap.
+func TemplateHelper() func(*http.Request) interface{} {
+ return func(r *http.Request) interface{} {
+ no := func() *admindb.User { return nil }
+
+ user := FromContext(r.Context())
+ if user == nil {
+ return no
+ }
+
+ yes := func() *admindb.User { return user }
+ return yes
+ }
+}
From fd5312b04a5d869a5440d6dc949960a1242c8f10 Mon Sep 17 00:00:00 2001
From: Henry
Date: Thu, 4 Mar 2021 11:15:09 +0100
Subject: [PATCH 05/12] disambiguate redirect url
---
web/handlers/admin/allow_list.go | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/web/handlers/admin/allow_list.go b/web/handlers/admin/allow_list.go
index 55d251ff..4b5f2ea6 100644
--- a/web/handlers/admin/allow_list.go
+++ b/web/handlers/admin/allow_list.go
@@ -20,7 +20,7 @@ type allowListH struct {
al admindb.AllowListService
}
-const redirectTo = "/admin/members"
+const redirectToMembers = "/admin/members"
func (h allowListH) add(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
@@ -57,7 +57,7 @@ func (h allowListH) add(w http.ResponseWriter, req *http.Request) {
return
}
- http.Redirect(w, req, redirectTo, http.StatusFound)
+ http.Redirect(w, req, redirectToMembers, http.StatusFound)
}
func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
@@ -93,7 +93,7 @@ func (h allowListH) removeConfirm(rw http.ResponseWriter, req *http.Request) (in
entry, err := h.al.GetByID(req.Context(), id)
if err != nil {
if errors.Is(err, admindb.ErrNotFound) {
- http.Redirect(rw, req, redirectTo, http.StatusFound)
+ http.Redirect(rw, req, redirectToMembers, http.StatusFound)
return nil, ErrRedirected
}
return nil, err
@@ -109,7 +109,7 @@ func (h allowListH) remove(rw http.ResponseWriter, req *http.Request) {
if err != nil {
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
// TODO "flash" errors
- http.Redirect(rw, req, redirectTo, http.StatusFound)
+ http.Redirect(rw, req, redirectToMembers, http.StatusFound)
return
}
@@ -117,7 +117,7 @@ func (h allowListH) remove(rw http.ResponseWriter, req *http.Request) {
if err != nil {
err = weberrors.ErrBadRequest{Where: "ID", Details: err}
// TODO "flash" errors
- http.Redirect(rw, req, redirectTo, http.StatusFound)
+ http.Redirect(rw, req, redirectToMembers, http.StatusFound)
return
}
@@ -132,5 +132,5 @@ func (h allowListH) remove(rw http.ResponseWriter, req *http.Request) {
status = http.StatusNotFound
}
- http.Redirect(rw, req, redirectTo, status)
+ http.Redirect(rw, req, redirectToMembers, status)
}
From 9f1fef1916b4d1d51bb9669a4569d601e1459041 Mon Sep 17 00:00:00 2001
From: Henry
Date: Thu, 4 Mar 2021 12:26:49 +0100
Subject: [PATCH 06/12] more consistent type names for handlers
---
web/handlers/admin/allow_list.go | 10 +++++-----
web/handlers/admin/handler.go | 4 ++--
web/handlers/admin/invites.go | 6 +++---
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/web/handlers/admin/allow_list.go b/web/handlers/admin/allow_list.go
index 4b5f2ea6..bb4182b5 100644
--- a/web/handlers/admin/allow_list.go
+++ b/web/handlers/admin/allow_list.go
@@ -14,7 +14,7 @@ import (
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
)
-type allowListH struct {
+type allowListHandler struct {
r *render.Renderer
al admindb.AllowListService
@@ -22,7 +22,7 @@ type allowListH struct {
const redirectToMembers = "/admin/members"
-func (h allowListH) add(w http.ResponseWriter, req *http.Request) {
+func (h allowListHandler) 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"))
@@ -60,7 +60,7 @@ func (h allowListH) add(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, redirectToMembers, http.StatusFound)
}
-func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+func (h allowListHandler) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
lst, err := h.al.List(req.Context())
if err != nil {
return nil, err
@@ -83,7 +83,7 @@ func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interfa
// TODO: move to render package so that we can decide to not render a page during the controller
var ErrRedirected = errors.New("render: not rendered but redirected")
-func (h allowListH) removeConfirm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+func (h allowListHandler) removeConfirm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
id, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
if err != nil {
err = weberrors.ErrBadRequest{Where: "ID", Details: err}
@@ -104,7 +104,7 @@ func (h allowListH) removeConfirm(rw http.ResponseWriter, req *http.Request) (in
}, nil
}
-func (h allowListH) remove(rw http.ResponseWriter, req *http.Request) {
+func (h allowListHandler) remove(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go
index 3549e293..75db4dfa 100644
--- a/web/handlers/admin/handler.go
+++ b/web/handlers/admin/handler.go
@@ -55,7 +55,7 @@ func Handler(
return map[string]interface{}{}, nil
}))
- var ah = allowListH{
+ var ah = allowListHandler{
r: r,
al: al,
}
@@ -64,7 +64,7 @@ func Handler(
mux.HandleFunc("/members/remove/confirm", r.HTML("admin/allow-list-remove-confirm.tmpl", ah.removeConfirm))
mux.HandleFunc("/members/remove", ah.remove)
- var ih = invitesH{
+ var ih = invitesHandler{
r: r,
db: is,
}
diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go
index 710cd856..5f159969 100644
--- a/web/handlers/admin/invites.go
+++ b/web/handlers/admin/invites.go
@@ -11,13 +11,13 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/web/user"
)
-type invitesH struct {
+type invitesHandler struct {
r *render.Renderer
db admindb.InviteService
}
-func (h invitesH) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+func (h invitesHandler) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
lst, err := h.db.List(req.Context())
if err != nil {
return nil, err
@@ -38,7 +38,7 @@ func (h invitesH) overview(rw http.ResponseWriter, req *http.Request) (interface
return pageData, nil
}
-func (h invitesH) create(w http.ResponseWriter, req *http.Request) {
+func (h invitesHandler) create(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"))
From 98468e93a5b09dfbe1508630e1c2470145c17992 Mon Sep 17 00:00:00 2001
From: Henry
Date: Thu, 4 Mar 2021 15:09:14 +0100
Subject: [PATCH 07/12] Invite endpoints
* Add revoke and created templates
* Render invite accept with domain
* Flesh out accept page
---
cmd/server/main.go | 1 +
web/handlers/admin/allow_list.go | 1 +
web/handlers/admin/allow_list_test.go | 2 +-
web/handlers/admin/app_test.go | 40 ++++++-
web/handlers/admin/handler.go | 16 ++-
web/handlers/admin/invites.go | 101 +++++++++++++++---
web/handlers/admin/invites_test.go | 43 +++++++-
web/handlers/http.go | 24 +++--
web/handlers/http_test.go | 14 +--
web/handlers/invites.go | 60 +++++++++++
web/handlers/invites_test.go | 1 +
web/handlers/setup_test.go | 4 +
web/i18n/defaults/active.en.toml | 20 +++-
web/router/admin.go | 8 +-
web/router/complete.go | 6 ++
web/templates/admin/invite-created.tmpl | 23 ++++
.../admin/{invites.tmpl => invite-list.tmpl} | 10 +-
.../admin/invite-revoke-confirm.tmpl | 35 ++++++
web/templates/invite/accept.tmpl | 16 +++
web/templates/invite/consumed.tmpl | 12 +++
web/user/testing.go | 21 ++++
web/utils.go | 1 +
22 files changed, 414 insertions(+), 45 deletions(-)
create mode 100644 web/handlers/invites.go
create mode 100644 web/handlers/invites_test.go
create mode 100644 web/templates/admin/invite-created.tmpl
rename web/templates/admin/{invites.tmpl => invite-list.tmpl} (89%)
create mode 100644 web/templates/admin/invite-revoke-confirm.tmpl
create mode 100644 web/templates/invite/accept.tmpl
create mode 100644 web/templates/invite/consumed.tmpl
create mode 100644 web/user/testing.go
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 58a6184c..65ab6d4f 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -213,6 +213,7 @@ func runroomsrv() error {
dashboardH, err := handlers.New(
kitlog.With(log, "package", "web"),
repo.New(repoDir),
+ httpsDomain,
roomsrv.StateManager,
db.AuthWithSSB,
db.AuthFallback,
diff --git a/web/handlers/admin/allow_list.go b/web/handlers/admin/allow_list.go
index bb4182b5..de989ce8 100644
--- a/web/handlers/admin/allow_list.go
+++ b/web/handlers/admin/allow_list.go
@@ -98,6 +98,7 @@ func (h allowListHandler) removeConfirm(rw http.ResponseWriter, req *http.Reques
}
return nil, err
}
+
return map[string]interface{}{
"Entry": entry,
csrf.TemplateTag: csrf.TemplateField(req),
diff --git a/web/handlers/admin/allow_list_test.go b/web/handlers/admin/allow_list_test.go
index d825acb6..68726fbd 100644
--- a/web/handlers/admin/allow_list_test.go
+++ b/web/handlers/admin/allow_list_test.go
@@ -149,7 +149,7 @@ func TestAllowList(t *testing.T) {
// check for link to remove confirm link
link, yes := elems.ContentsFiltered("a").Attr("href")
a.True(yes, "a-tag has href attribute")
- a.Equal("/members/remove/confirm?id=666", link)
+ a.Equal("/admin/members/remove/confirm?id=666", link)
}
func TestAllowListRemoveConfirmation(t *testing.T) {
diff --git a/web/handlers/admin/app_test.go b/web/handlers/admin/app_test.go
index 6c36aa98..4ccba985 100644
--- a/web/handlers/admin/app_test.go
+++ b/web/handlers/admin/app_test.go
@@ -4,6 +4,7 @@ package admin
import (
"context"
+ "math/rand"
"net/http"
"testing"
@@ -18,6 +19,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
+ "github.com/ssb-ngi-pointer/go-ssb-room/web/user"
)
type testSession struct {
@@ -30,6 +32,10 @@ type testSession struct {
NoticeDB *mockdb.FakeNoticesService
InvitesDB *mockdb.FakeInviteService
+ User *admindb.User
+
+ Domain string
+
RoomState *roomstate.Manager
}
@@ -46,7 +52,15 @@ func newSession(t *testing.T) *testSession {
ctx := context.TODO()
ts.RoomState = roomstate.NewManager(ctx, log)
- ts.Router = router.Admin(nil)
+ ts.Router = router.CompleteApp()
+
+ ts.Domain = randomString(10)
+
+ // fake user
+ ts.User = &admindb.User{
+ ID: 1234,
+ Name: "room mate",
+ }
// setup rendering
@@ -77,14 +91,34 @@ func newSession(t *testing.T) *testSession {
}
ts.Mux = http.NewServeMux()
- ts.Mux.Handle("/", Handler(r,
+
+ handler := Handler(
+ ts.Domain,
+ r,
ts.RoomState,
ts.AllowListDB,
ts.InvitesDB,
ts.NoticeDB,
ts.PinnedDB,
- ))
+ )
+
+ handler = user.MiddlewareForTests(ts.User)(handler)
+
+ ts.Mux.Handle("/", handler)
+
ts.Client = tester.New(ts.Mux, t)
return &ts
}
+
+// utils
+
+func randomString(n int) string {
+ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+ s := make([]rune, n)
+ for i := range s {
+ s[i] = letters[rand.Intn(len(letters))]
+ }
+ return string(s)
+}
diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go
index 75db4dfa..aa09b2a5 100644
--- a/web/handlers/admin/handler.go
+++ b/web/handlers/admin/handler.go
@@ -9,13 +9,12 @@ import (
"strconv"
"strings"
- "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
"github.com/vcraescu/go-paginator/v2"
"github.com/vcraescu/go-paginator/v2/adapter"
"github.com/vcraescu/go-paginator/v2/view"
-
"go.mindeco.de/http/render"
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
)
@@ -26,7 +25,9 @@ var HTMLTemplates = []string{
"admin/allow-list.tmpl",
"admin/allow-list-remove-confirm.tmpl",
- "admin/invites.tmpl",
+ "admin/invite-list.tmpl",
+ "admin/invite-revoke-confirm.tmpl",
+ "admin/invite-created.tmpl",
"admin/notice-edit.tmpl",
}
@@ -34,6 +35,7 @@ var HTMLTemplates = []string{
// Handler supplies the elevated access pages to known users.
// It is not registering on the mux router like other pages to clean up the authorize flow.
func Handler(
+ domainName string,
r *render.Renderer,
roomState *roomstate.Manager,
al admindb.AllowListService,
@@ -67,9 +69,13 @@ func Handler(
var ih = invitesHandler{
r: r,
db: is,
+
+ domainName: domainName,
}
- mux.HandleFunc("/invites", r.HTML("admin/invites.tmpl", ih.overview))
- mux.HandleFunc("/invites/create", ih.create)
+ mux.HandleFunc("/invites", r.HTML("admin/invite-list.tmpl", ih.overview))
+ mux.HandleFunc("/invites/create", r.HTML("admin/invite-created.tmpl", ih.create))
+ mux.HandleFunc("/invites/revoke/confirm", r.HTML("admin/invite-revoke-confirm.tmpl", ih.revokeConfirm))
+ mux.HandleFunc("/invites/revoke", ih.revoke)
var nh = noticeHandler{
r: r,
diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go
index 5f159969..8fd9e6ee 100644
--- a/web/handlers/admin/invites.go
+++ b/web/handlers/admin/invites.go
@@ -1,13 +1,18 @@
package admin
import (
+ "errors"
"fmt"
"net/http"
+ "strconv"
+ "github.com/gorilla/csrf"
"go.mindeco.de/http/render"
- "github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "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/user"
)
@@ -15,6 +20,8 @@ type invitesHandler struct {
r *render.Renderer
db admindb.InviteService
+
+ domainName string
}
func (h invitesHandler) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
@@ -34,38 +41,106 @@ func (h invitesHandler) overview(rw http.ResponseWriter, req *http.Request) (int
}
pageData[csrf.TemplateTag] = csrf.TemplateField(req)
-
return pageData, nil
}
-func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) {
+func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "POST" {
// TODO: proper error type
- h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
- return
+ return nil, fmt.Errorf("bad request")
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
- h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
- return
+ return nil, fmt.Errorf("bad request: %w", err)
}
user := user.FromContext(req.Context())
if user == nil {
- err := fmt.Errorf("warning: no user session for elevated access request")
- h.r.Error(w, req, http.StatusInternalServerError, err)
- return
+ return nil, fmt.Errorf("warning: no user session for elevated access request")
}
aliasSuggestion := req.Form.Get("alias_suggestion")
token, err := h.db.Create(req.Context(), user.ID, aliasSuggestion)
if err != nil {
- h.r.Error(w, req, http.StatusInternalServerError, err)
+ return nil, err
+ }
+
+ urlTo := web.NewURLTo(router.CompleteApp())
+ acceptURL := urlTo(router.CompleteInviteAccept, "token", token)
+ acceptURL.Host = h.domainName
+ acceptURL.Scheme = "https"
+
+ return map[string]interface{}{
+ "Token": token,
+ "AccepURL": acceptURL.String(),
+
+ "AliasSuggestion": aliasSuggestion,
+ }, nil
+}
+
+func (h invitesHandler) revokeConfirm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+ id, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
+ if err != nil {
+ err = weberrors.ErrBadRequest{Where: "ID", Details: err}
+ return nil, err
+ }
+
+ // TODO: add GetByID to invite service
+ var invite admindb.Invite
+ list, err := h.db.List(req.Context())
+ if err != nil {
+ return nil, err
+ }
+
+ found := false
+ for _, elem := range list {
+ if elem.ID == id {
+ invite = elem
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ return nil, weberrors.ErrNotFound{What: "invite"}
+ }
+
+ return map[string]interface{}{
+ "Invite": invite,
+ csrf.TemplateTag: csrf.TemplateField(req),
+ }, nil
+}
+
+const redirectToInvites = "/admin/invites"
+
+func (h invitesHandler) revoke(rw http.ResponseWriter, req *http.Request) {
+ err := req.ParseForm()
+ if err != nil {
+ err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
+ // TODO "flash" errors
+ http.Redirect(rw, req, redirectToInvites, http.StatusFound)
return
}
- fmt.Println("use me:", token)
+ id, err := strconv.ParseInt(req.FormValue("id"), 10, 64)
+ if err != nil {
+ err = weberrors.ErrBadRequest{Where: "ID", Details: err}
+ // TODO "flash" errors
+ http.Redirect(rw, req, redirectToInvites, http.StatusFound)
+ return
+ }
+
+ status := http.StatusFound
+ err = h.db.Revoke(req.Context(), id)
+ if err != nil {
+ if !errors.Is(err, admindb.ErrNotFound) {
+ // TODO "flash" errors
+ h.r.Error(rw, req, http.StatusInternalServerError, err)
+ return
+ }
+ status = http.StatusNotFound
+ }
- http.Redirect(w, req, "/admin/invites", http.StatusFound)
+ http.Redirect(rw, req, redirectToInvites, status)
}
diff --git a/web/handlers/admin/invites_test.go b/web/handlers/admin/invites_test.go
index 3816c6fc..d663dae5 100644
--- a/web/handlers/admin/invites_test.go
+++ b/web/handlers/admin/invites_test.go
@@ -2,10 +2,15 @@ package admin
import (
"net/http"
+ "net/url"
"testing"
- "github.com/ssb-ngi-pointer/go-ssb-room/web/router"
+ "github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/web"
+ "github.com/ssb-ngi-pointer/go-ssb-room/web/router"
)
func TestInvitesCreateForm(t *testing.T) {
@@ -45,3 +50,39 @@ func TestInvitesCreateForm(t *testing.T) {
a.True(ok, "input has a name")
a.Equal("alias_suggestion", name, "wrong name on input field")
}
+
+func TestInvitesCreate(t *testing.T) {
+ ts := newSession(t)
+ a := assert.New(t)
+
+ urlTo := web.NewURLTo(ts.Router)
+ urlRemove := urlTo(router.AdminInvitesCreate)
+
+ testInvite := "your-fake-test-invite"
+ ts.InvitesDB.CreateReturns(testInvite, nil)
+
+ rec := ts.Client.PostForm(urlRemove.String(), url.Values{
+ "alias_suggestion": []string{"jerry"},
+ })
+ a.Equal(http.StatusOK, rec.Code)
+
+ a.Equal(1, ts.InvitesDB.CreateCallCount())
+ _, userID, aliasSuggestion := ts.InvitesDB.CreateArgsForCall(0)
+ a.EqualValues(ts.User.ID, userID)
+ a.EqualValues("jerry", aliasSuggestion)
+
+ doc, err := goquery.NewDocumentFromReader(rec.Body)
+ require.NoError(t, err, "failed to parse response")
+
+ assertLocalized(t, doc, []localizedElement{
+ {"title", "AdminInviteCreatedTitle"},
+ {"#welcome", "AdminInviteCreatedWelcome"},
+ })
+
+ wantURL := urlTo(router.CompleteInviteAccept, "token", testInvite)
+ wantURL.Host = ts.Domain
+ wantURL.Scheme = "https"
+
+ shownLink := doc.Find("#invite-accept-link").Text()
+ a.Equal(wantURL.String(), shownLink)
+}
diff --git a/web/handlers/http.go b/web/handlers/http.go
index 7cde53ec..2e892112 100644
--- a/web/handlers/http.go
+++ b/web/handlers/http.go
@@ -32,6 +32,8 @@ import (
var HTMLTemplates = []string{
"landing/index.tmpl",
"landing/about.tmpl",
+ "invite/accept.tmpl",
+ "invite/consumed.tmpl",
"notice/list.tmpl",
"notice/show.tmpl",
"error.tmpl",
@@ -41,6 +43,7 @@ var HTMLTemplates = []string{
func New(
logger logging.Interface,
repo repo.Interface,
+ domainName string,
roomState *roomstate.Manager,
as admindb.AuthWithSSBService,
fs admindb.AuthFallbackService,
@@ -201,7 +204,9 @@ func New(
// hookup handlers to the router
roomsAuth.Handler(m, r, a)
- adminHandler := a.Authenticate(admin.Handler(r,
+ adminHandler := a.Authenticate(admin.Handler(
+ domainName,
+ r,
roomState,
al,
is,
@@ -224,11 +229,18 @@ func New(
}))
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
- var nr noticeHandler
- nr.notices = ns
- nr.pinned = ps
- m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nr.list))
- m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nr.show))
+ var nh = noticeHandler{
+ notices: ns,
+ pinned: ps,
+ }
+ m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nh.list))
+ m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nh.show))
+
+ var ih = inviteHandler{
+ invites: is,
+ }
+ 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.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
diff --git a/web/handlers/http_test.go b/web/handlers/http_test.go
index aa7c54fc..89a5c44f 100644
--- a/web/handlers/http_test.go
+++ b/web/handlers/http_test.go
@@ -68,7 +68,6 @@ func TestRestricted(t *testing.T) {
a := assert.New(t)
testURLs := []string{
- // "/admin/",
"/admin/admin",
"/admin/admin/",
}
@@ -113,13 +112,13 @@ func TestFallbackAuth(t *testing.T) {
doc, resp := ts.Client.GetHTML(signInFormURL.String())
a.Equal(http.StatusOK, resp.Code)
- assertCSRFTokenPresent(t, doc.Find("form"))
-
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 1, "should have one cookie for CSRF protection validation")
- t.Log(csrfCookie)
+
jar.SetCookies(signInFormURL, csrfCookie)
+ assertCSRFTokenPresent(t, doc.Find("form"))
+
csrfTokenElem := doc.Find("input[type=hidden]")
a.Equal(1, csrfTokenElem.Length())
@@ -137,8 +136,6 @@ func TestFallbackAuth(t *testing.T) {
}
ts.AuthFallbackDB.CheckReturns(int64(23), nil)
- t.Log(loginVals)
-
signInURL, err := ts.Router.Get(router.AuthFallbackSignIn).URL()
r.Nil(err)
@@ -214,7 +211,6 @@ func TestFallbackAuth(t *testing.T) {
html, resp = ts.Client.GetHTML(durl)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
- t.Log(html.Find("body").Text())
assertLocalized(t, html, []localizedElement{
{"#welcome", "AdminDashboardWelcome"},
{"title", "AdminDashboardTitle"},
@@ -224,7 +220,6 @@ func TestFallbackAuth(t *testing.T) {
}
// utils
-
type localizedElement struct {
Selector, Label string
}
@@ -238,8 +233,7 @@ func assertLocalized(t *testing.T, html *goquery.Document, elems []localizedElem
func assertCSRFTokenPresent(t *testing.T, sel *goquery.Selection) {
a := assert.New(t)
-
- csrfField := sel.Find("input[name=gorilla.csrf.Token]")
+ csrfField := sel.Find("input[name='gorilla.csrf.Token']")
a.EqualValues(1, csrfField.Length(), "no csrf-token input tag")
tipe, ok := csrfField.Attr("type")
a.True(ok, "csrf input has a type")
diff --git a/web/handlers/invites.go b/web/handlers/invites.go
new file mode 100644
index 00000000..99ea66e8
--- /dev/null
+++ b/web/handlers/invites.go
@@ -0,0 +1,60 @@
+package handlers
+
+import (
+ "errors"
+ "net/http"
+
+ "go.mindeco.de/http/render"
+
+ "github.com/gorilla/csrf"
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
+ refs "go.mindeco.de/ssb-refs"
+)
+
+type inviteHandler struct {
+ r *render.Renderer
+
+ invites admindb.InviteService
+ alaises admindb.AliasService
+}
+
+func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+ inv, err := h.invites.GetByToken(req.Context(), req.URL.Query().Get("token"))
+ if err != nil {
+ if errors.Is(err, admindb.ErrNotFound) {
+ return nil, weberrors.ErrNotFound{What: "invite"}
+ }
+ return nil, err
+ }
+
+ return map[string]interface{}{
+ "Invite": inv,
+ csrf.TemplateTag: csrf.TemplateField(req),
+ }, 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}
+ }
+
+ token := req.FormValue("token")
+
+ newMember, err := refs.ParseFeedRef(req.FormValue("new_member"))
+ if err != nil {
+ return nil, weberrors.ErrBadRequest{Where: "form data", Details: err}
+ }
+
+ inv, err := h.invites.Consume(req.Context(), token, *newMember)
+ if err != nil {
+ if errors.Is(err, admindb.ErrNotFound) {
+ return nil, weberrors.ErrNotFound{What: "invite"}
+ }
+ return nil, err
+ }
+
+ return map[string]interface{}{
+ "TunnelAddress": "pew pew",
+ }, nil
+}
diff --git a/web/handlers/invites_test.go b/web/handlers/invites_test.go
new file mode 100644
index 00000000..5ac8282f
--- /dev/null
+++ b/web/handlers/invites_test.go
@@ -0,0 +1 @@
+package handlers
diff --git a/web/handlers/setup_test.go b/web/handlers/setup_test.go
index fc918fb1..76093826 100644
--- a/web/handlers/setup_test.go
+++ b/web/handlers/setup_test.go
@@ -34,6 +34,7 @@ type testSession struct {
AuthDB *mockdb.FakeAuthWithSSBService
AuthFallbackDB *mockdb.FakeAuthFallbackService
AllowListDB *mockdb.FakeAllowListService
+ InvitesDB *mockdb.FakeInviteService
PinnedDB *mockdb.FakePinnedNoticesService
NoticeDB *mockdb.FakeNoticesService
@@ -59,6 +60,7 @@ func setup(t *testing.T) *testSession {
ts.AuthDB = new(mockdb.FakeAuthWithSSBService)
ts.AuthFallbackDB = new(mockdb.FakeAuthFallbackService)
ts.AllowListDB = new(mockdb.FakeAllowListService)
+ ts.InvitesDB = new(mockdb.FakeInviteService)
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
defaultNotice := &admindb.Notice{
Title: "Default Notice Title",
@@ -76,10 +78,12 @@ func setup(t *testing.T) *testSession {
h, err := New(
log,
testRepo,
+ "localhost",
ts.RoomState,
ts.AuthDB,
ts.AuthFallbackDB,
ts.AllowListDB,
+ ts.InvitesDB,
ts.NoticeDB,
ts.PinnedDB,
)
diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml
index cd77251b..3def3043 100644
--- a/web/i18n/defaults/active.en.toml
+++ b/web/i18n/defaults/active.en.toml
@@ -24,15 +24,31 @@ AdminAllowListRemoveConfirmWelcome = "Are you sure you want to remove this membe
AdminAllowListRemoveConfirmTitle = "Confirm member removal"
AdminInvitesTitle = "Invites"
-AdminInvitesWelcome = "Here ytou can create invite tokens for people who are not yet members of this room."
+AdminInvitesWelcome = "Create invite tokens for people who are not yet members of this room."
AdminInvitesAliasSuggestion = "Suggested alias (optional)"
-AdminInvitesRevoke = "Revoke"
+AdminInviteRevoke = "Revoke"
+AdminInviteRevokeConfirmTitle = "Confirm invite revocation"
+AdminInviteRevokeConfirmWelcome = "Are you sure you want to remove this invite? If you already sent it out, they will not be able to use it."
+
+# TODO: add placeholder support to the template helpers (https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60)
+AdminInviteCreatedBy = "Created by:"
+AdminInviteSuggestedAliasIs = "The suggested alias is:"
+AdminInviteSuggestedAliasIsShort = "Alias:"
+
+AdminInviteCreatedTitle = "Invite created successfully"
+AdminInviteCreatedWelcome = "Here is the invite you created. Please copy it before closing the page or it will be lost."
+
NavAdminLanding = "Home"
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:"
+
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."
diff --git a/web/router/admin.go b/web/router/admin.go
index 09175394..0848875a 100644
--- a/web/router/admin.go
+++ b/web/router/admin.go
@@ -14,8 +14,10 @@ const (
AdminAllowListRemoveConfirm = "admin:allow-list:remove:confirm"
AdminAllowListRemove = "admin:allow-list:remove"
- AdminInvitesOverview = "admin:invites:overview"
- AdminInvitesCreate = "admin:invites:create"
+ AdminInvitesOverview = "admin:invites:overview"
+ AdminInvitesRevokeConfirm = "admin:invites:revoke:confirm"
+ AdminInvitesRevoke = "admin:invites:revoke"
+ AdminInvitesCreate = "admin:invites:create"
AdminNoticeEdit = "admin:notice:edit"
AdminNoticeSave = "admin:notice:save"
@@ -43,6 +45,8 @@ func Admin(m *mux.Router) *mux.Router {
m.Path("/notice/save").Methods("POST").Name(AdminNoticeSave)
m.Path("/invites").Methods("GET").Name(AdminInvitesOverview)
+ m.Path("/invites/revoke/confirm").Methods("GET").Name(AdminInvitesRevokeConfirm)
+ m.Path("/invites/revoke").Methods("POST").Name(AdminInvitesRevoke)
m.Path("/invites/create").Methods("POST").Name(AdminInvitesCreate)
return m
diff --git a/web/router/complete.go b/web/router/complete.go
index e66fa141..134a2e21 100644
--- a/web/router/complete.go
+++ b/web/router/complete.go
@@ -13,6 +13,9 @@ const (
CompleteNoticeShow = "complete:notice:show"
CompleteNoticeList = "complete:notice:list"
+
+ CompleteInviteAccept = "complete:invite:accept"
+ CompleteInviteConsume = "complete:invite:consume"
)
// CompleteApp constructs a mux.Router containing the routes for batch Complete html frontend
@@ -25,6 +28,9 @@ func CompleteApp() *mux.Router {
m.Path("/").Methods("GET").Name(CompleteIndex)
m.Path("/about").Methods("GET").Name(CompleteAbout)
+ m.Path("/invite/accept").Methods("GET").Name(CompleteInviteAccept)
+ m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)
+
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)
m.Path("/notice/list").Methods("GET").Name(CompleteNoticeList)
diff --git a/web/templates/admin/invite-created.tmpl b/web/templates/admin/invite-created.tmpl
new file mode 100644
index 00000000..2dc2c7f6
--- /dev/null
+++ b/web/templates/admin/invite-created.tmpl
@@ -0,0 +1,23 @@
+{{ define "title" }}{{i18n "AdminInviteCreatedTitle"}}{{ end }}
+{{ define "content" }}
+
+
+
{{i18n "AdminInviteCreatedWelcome"}}
+
+
{{i18n "InviteAccept"}}
+
+
{{.AccepURL}}
+
+ {{if ne .AliasSuggestion ""}}
+
+
{{i18n "AdminInviteSuggestedAliasIs"}} {{.AliasSuggestion}}
+ {{end}}
+
+{{end}}
\ No newline at end of file
diff --git a/web/templates/admin/invites.tmpl b/web/templates/admin/invite-list.tmpl
similarity index 89%
rename from web/templates/admin/invites.tmpl
rename to web/templates/admin/invite-list.tmpl
index 85ee711a..c1eb29db 100644
--- a/web/templates/admin/invites.tmpl
+++ b/web/templates/admin/invite-list.tmpl
@@ -36,12 +36,18 @@
{{.CreatedBy.Name}}
+ >
+
+ {{i18n "AdminInviteCreatedBy"}} {{.CreatedBy.Name}}
+ {{if ne .AliasSuggestion ""}}
+ ({{i18n "AdminInviteSuggestedAliasIsShort"}} {{.AliasSuggestion}})
+ {{end}}
+
{{i18n "AdminInvitesRevoke"}}
+ >{{i18n "AdminInviteRevoke"}}
{{end}}
diff --git a/web/templates/admin/invite-revoke-confirm.tmpl b/web/templates/admin/invite-revoke-confirm.tmpl
new file mode 100644
index 00000000..1cf688de
--- /dev/null
+++ b/web/templates/admin/invite-revoke-confirm.tmpl
@@ -0,0 +1,35 @@
+{{ define "title" }}{{i18n "AdminInviteRevokeConfirmTitle"}}{{ end }}
+{{ define "content" }}
+
+
+
{{i18n "AdminInviteRevokeConfirmWelcome"}}
+
+
{{.Invite.CreatedBy.Name}}
+
+ {{if ne .Invite.AliasSuggestion ""}}
+
+
{{i18n "AdminInviteSuggestedAliasIs"}} {{.Invite.AliasSuggestion}}
+ {{end}}
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/web/templates/invite/accept.tmpl b/web/templates/invite/accept.tmpl
new file mode 100644
index 00000000..8157be14
--- /dev/null
+++ b/web/templates/invite/accept.tmpl
@@ -0,0 +1,16 @@
+{{ define "title" }}{{i18n "InviteAcceptTitle"}}{{ end }}
+{{ define "content" }}
+
+
+
{{i18n "InviteAcceptWelcome"}}
+
+
+ {{if ne .AliasSuggestion ""}}
+
+
{{i18n "InviteAcceptAliasSuggestion"}} {{.AliasSuggestion}}
+ {{end}}
+
+{{end}}
\ No newline at end of file
diff --git a/web/templates/invite/consumed.tmpl b/web/templates/invite/consumed.tmpl
new file mode 100644
index 00000000..cefcb3cf
--- /dev/null
+++ b/web/templates/invite/consumed.tmpl
@@ -0,0 +1,12 @@
+{{ define "title" }}{{i18n "InviteConsumedTitle"}}{{ end }}
+{{ define "content" }}
+
+
+
{{i18n "InviteConsumedWelcome"}}
+
+
TODO: present tunnel address and ssb uri redirect
+
+{{end}}
\ No newline at end of file
diff --git a/web/user/testing.go b/web/user/testing.go
new file mode 100644
index 00000000..e204b625
--- /dev/null
+++ b/web/user/testing.go
@@ -0,0 +1,21 @@
+package user
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+)
+
+// MiddlewareForTests gives us a way to inject _test users_. It should not be used in production.
+// This is exists here because we need to use roomUserContextKey which shouldn't be exported either.
+// TODO: could be protected with an extra build tag.
+// (Sadly +build test does not exist https://github.com/golang/go/issues/21360 )
+func MiddlewareForTests(user *admindb.User) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ ctx := context.WithValue(req.Context(), roomUserContextKey, user)
+ next.ServeHTTP(w, req.WithContext(ctx))
+ })
+ }
+}
diff --git a/web/utils.go b/web/utils.go
index ebbd91fd..e4709355 100644
--- a/web/utils.go
+++ b/web/utils.go
@@ -62,6 +62,7 @@ func NewURLTo(appRouter *mux.Router) func(string, ...interface{}) *url.URL {
params = append(params, v.Ref())
default:
level.Error(l).Log("msg", "invalid param type", "param", fmt.Sprintf("%T", p), "route", routeName)
+ return &url.URL{}
}
}
From 672647cd4dbf5290e2b34ad26d201db730585074 Mon Sep 17 00:00:00 2001
From: Henry
Date: Fri, 5 Mar 2021 10:46:59 +0100
Subject: [PATCH 08/12] Add GetByID and GetByToken to invite service
---
admindb/interface.go | 10 ++-
admindb/mockdb/invite.go | 162 ++++++++++++++++++++++++++++++++++
admindb/sqlite/invites.go | 74 +++++++++++++---
web/handlers/admin/invites.go | 20 +----
4 files changed, 236 insertions(+), 30 deletions(-)
diff --git a/admindb/interface.go b/admindb/interface.go
index f2592f0d..ce7b6c9a 100644
--- a/admindb/interface.go
+++ b/admindb/interface.go
@@ -57,16 +57,22 @@ type AliasService interface{}
// InviteService manages creation and consumption of invite tokens for joining the room.
type InviteService interface {
-
// Create creates a new invite for a new member. It returns the token or an error.
// createdBy is user ID of the admin or moderator who created it.
// aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21)
Create(ctx context.Context, createdBy int64, aliasSuggestion string) (string, error)
- // Consume checks if the passed token is still valid. If it is it adds newMember to the members of the room and invalidates the token.
+ // Consume checks if the passed token is still valid.
+ // If it is it adds newMember to the members of the room and invalidates the token.
// If the token isn't valid, it returns an error.
Consume(ctx context.Context, token string, newMember refs.FeedRef) (Invite, error)
+ // GetByToken returns the Invite if one for that token exists, or an error
+ GetByToken(ctx context.Context, token string) (Invite, error)
+
+ // GetByToken returns the Invite if one for that ID exists, or an error
+ GetByID(ctx context.Context, id int64) (Invite, error)
+
// List returns a list of all the valid invites
List(ctx context.Context) ([]Invite, error)
diff --git a/admindb/mockdb/invite.go b/admindb/mockdb/invite.go
index 8a38230b..080a8a60 100644
--- a/admindb/mockdb/invite.go
+++ b/admindb/mockdb/invite.go
@@ -40,6 +40,34 @@ type FakeInviteService struct {
result1 string
result2 error
}
+ GetByIDStub func(context.Context, int64) (admindb.Invite, error)
+ getByIDMutex sync.RWMutex
+ getByIDArgsForCall []struct {
+ arg1 context.Context
+ arg2 int64
+ }
+ getByIDReturns struct {
+ result1 admindb.Invite
+ result2 error
+ }
+ getByIDReturnsOnCall map[int]struct {
+ result1 admindb.Invite
+ result2 error
+ }
+ GetByTokenStub func(context.Context, string) (admindb.Invite, error)
+ getByTokenMutex sync.RWMutex
+ getByTokenArgsForCall []struct {
+ arg1 context.Context
+ arg2 string
+ }
+ getByTokenReturns struct {
+ result1 admindb.Invite
+ result2 error
+ }
+ getByTokenReturnsOnCall map[int]struct {
+ result1 admindb.Invite
+ result2 error
+ }
ListStub func(context.Context) ([]admindb.Invite, error)
listMutex sync.RWMutex
listArgsForCall []struct {
@@ -201,6 +229,136 @@ func (fake *FakeInviteService) CreateReturnsOnCall(i int, result1 string, result
}{result1, result2}
}
+func (fake *FakeInviteService) GetByID(arg1 context.Context, arg2 int64) (admindb.Invite, error) {
+ fake.getByIDMutex.Lock()
+ ret, specificReturn := fake.getByIDReturnsOnCall[len(fake.getByIDArgsForCall)]
+ fake.getByIDArgsForCall = append(fake.getByIDArgsForCall, struct {
+ arg1 context.Context
+ arg2 int64
+ }{arg1, arg2})
+ stub := fake.GetByIDStub
+ fakeReturns := fake.getByIDReturns
+ fake.recordInvocation("GetByID", []interface{}{arg1, arg2})
+ fake.getByIDMutex.Unlock()
+ if stub != nil {
+ return stub(arg1, arg2)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeInviteService) GetByIDCallCount() int {
+ fake.getByIDMutex.RLock()
+ defer fake.getByIDMutex.RUnlock()
+ return len(fake.getByIDArgsForCall)
+}
+
+func (fake *FakeInviteService) GetByIDCalls(stub func(context.Context, int64) (admindb.Invite, error)) {
+ fake.getByIDMutex.Lock()
+ defer fake.getByIDMutex.Unlock()
+ fake.GetByIDStub = stub
+}
+
+func (fake *FakeInviteService) GetByIDArgsForCall(i int) (context.Context, int64) {
+ fake.getByIDMutex.RLock()
+ defer fake.getByIDMutex.RUnlock()
+ argsForCall := fake.getByIDArgsForCall[i]
+ return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeInviteService) GetByIDReturns(result1 admindb.Invite, result2 error) {
+ fake.getByIDMutex.Lock()
+ defer fake.getByIDMutex.Unlock()
+ fake.GetByIDStub = nil
+ fake.getByIDReturns = struct {
+ result1 admindb.Invite
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeInviteService) GetByIDReturnsOnCall(i int, result1 admindb.Invite, result2 error) {
+ fake.getByIDMutex.Lock()
+ defer fake.getByIDMutex.Unlock()
+ fake.GetByIDStub = nil
+ if fake.getByIDReturnsOnCall == nil {
+ fake.getByIDReturnsOnCall = make(map[int]struct {
+ result1 admindb.Invite
+ result2 error
+ })
+ }
+ fake.getByIDReturnsOnCall[i] = struct {
+ result1 admindb.Invite
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeInviteService) GetByToken(arg1 context.Context, arg2 string) (admindb.Invite, error) {
+ fake.getByTokenMutex.Lock()
+ ret, specificReturn := fake.getByTokenReturnsOnCall[len(fake.getByTokenArgsForCall)]
+ fake.getByTokenArgsForCall = append(fake.getByTokenArgsForCall, struct {
+ arg1 context.Context
+ arg2 string
+ }{arg1, arg2})
+ stub := fake.GetByTokenStub
+ fakeReturns := fake.getByTokenReturns
+ fake.recordInvocation("GetByToken", []interface{}{arg1, arg2})
+ fake.getByTokenMutex.Unlock()
+ if stub != nil {
+ return stub(arg1, arg2)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeInviteService) GetByTokenCallCount() int {
+ fake.getByTokenMutex.RLock()
+ defer fake.getByTokenMutex.RUnlock()
+ return len(fake.getByTokenArgsForCall)
+}
+
+func (fake *FakeInviteService) GetByTokenCalls(stub func(context.Context, string) (admindb.Invite, error)) {
+ fake.getByTokenMutex.Lock()
+ defer fake.getByTokenMutex.Unlock()
+ fake.GetByTokenStub = stub
+}
+
+func (fake *FakeInviteService) GetByTokenArgsForCall(i int) (context.Context, string) {
+ fake.getByTokenMutex.RLock()
+ defer fake.getByTokenMutex.RUnlock()
+ argsForCall := fake.getByTokenArgsForCall[i]
+ return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeInviteService) GetByTokenReturns(result1 admindb.Invite, result2 error) {
+ fake.getByTokenMutex.Lock()
+ defer fake.getByTokenMutex.Unlock()
+ fake.GetByTokenStub = nil
+ fake.getByTokenReturns = struct {
+ result1 admindb.Invite
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeInviteService) GetByTokenReturnsOnCall(i int, result1 admindb.Invite, result2 error) {
+ fake.getByTokenMutex.Lock()
+ defer fake.getByTokenMutex.Unlock()
+ fake.GetByTokenStub = nil
+ if fake.getByTokenReturnsOnCall == nil {
+ fake.getByTokenReturnsOnCall = make(map[int]struct {
+ result1 admindb.Invite
+ result2 error
+ })
+ }
+ fake.getByTokenReturnsOnCall[i] = struct {
+ result1 admindb.Invite
+ result2 error
+ }{result1, result2}
+}
+
func (fake *FakeInviteService) List(arg1 context.Context) ([]admindb.Invite, error) {
fake.listMutex.Lock()
ret, specificReturn := fake.listReturnsOnCall[len(fake.listArgsForCall)]
@@ -334,6 +492,10 @@ func (fake *FakeInviteService) Invocations() map[string][][]interface{} {
defer fake.consumeMutex.RUnlock()
fake.createMutex.RLock()
defer fake.createMutex.RUnlock()
+ fake.getByIDMutex.RLock()
+ defer fake.getByIDMutex.RUnlock()
+ fake.getByTokenMutex.RLock()
+ defer fake.getByTokenMutex.RUnlock()
fake.listMutex.RLock()
defer fake.listMutex.RUnlock()
fake.revokeMutex.RLock()
diff --git a/admindb/sqlite/invites.go b/admindb/sqlite/invites.go
index d9fe7cda..a6790201 100644
--- a/admindb/sqlite/invites.go
+++ b/admindb/sqlite/invites.go
@@ -29,8 +29,6 @@ type Invites struct {
allowList *AllowList
}
-const tokenLength = 50
-
// Create creates a new invite for a new member. It returns the token or an error.
// createdBy is user ID of the admin or moderator who created it.
// aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21)
@@ -90,20 +88,11 @@ func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion st
func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedRef) (admindb.Invite, error) {
var inv admindb.Invite
- tokenBytes, err := base64.URLEncoding.DecodeString(token)
+ hashedToken, err := getHashedToken(token)
if err != nil {
return inv, err
}
- if n := len(tokenBytes); n != tokenLength {
- return inv, fmt.Errorf("admindb: invalid invite token length (only got %d bytes)", n)
- }
-
- // hash the binary of the passed token
- h := sha256.New()
- h.Write(tokenBytes)
- hashedToken := fmt.Sprintf("%x", h.Sum(nil))
-
err = transact(i.db, func(tx *sql.Tx) error {
entry, err := models.Invites(
qm.Where("active = true AND token = ?", hashedToken),
@@ -153,6 +142,49 @@ func deleteConsumedInvites(tx boil.ContextExecutor) error {
return nil
}
+func (i Invites) GetByToken(ctx context.Context, token string) (admindb.Invite, error) {
+ var inv admindb.Invite
+
+ ht, err := getHashedToken(token)
+ if err != nil {
+ return inv, err
+ }
+
+ entry, err := models.Invites(
+ qm.Where("active = true AND token = ?", ht),
+ qm.Load("CreatedByAuthFallback"),
+ ).One(ctx, i.db)
+ if err != nil {
+ return inv, err
+ }
+
+ inv.ID = entry.ID
+ inv.AliasSuggestion = entry.AliasSuggestion
+ inv.CreatedBy.ID = entry.R.CreatedByAuthFallback.ID
+ inv.CreatedBy.Name = entry.R.CreatedByAuthFallback.Name
+
+ return inv, nil
+}
+
+func (i Invites) GetByID(ctx context.Context, id int64) (admindb.Invite, error) {
+ var inv admindb.Invite
+
+ entry, err := models.Invites(
+ qm.Where("active = true AND id = ?", id),
+ qm.Load("CreatedByAuthFallback"),
+ ).One(ctx, i.db)
+ if err != nil {
+ return inv, err
+ }
+
+ inv.ID = entry.ID
+ inv.AliasSuggestion = entry.AliasSuggestion
+ inv.CreatedBy.ID = entry.R.CreatedByAuthFallback.ID
+ inv.CreatedBy.Name = entry.R.CreatedByAuthFallback.Name
+
+ return inv, nil
+}
+
// List returns a list of all the valid invites
func (i Invites) List(ctx context.Context) ([]admindb.Invite, error) {
var invs []admindb.Invite
@@ -205,3 +237,21 @@ func (i Invites) Revoke(ctx context.Context, id int64) error {
return nil
})
}
+
+const tokenLength = 50
+
+func getHashedToken(b64tok string) (string, error) {
+ tokenBytes, err := base64.URLEncoding.DecodeString(b64tok)
+ if err != nil {
+ return "", err
+ }
+
+ if n := len(tokenBytes); n != tokenLength {
+ return "", fmt.Errorf("admindb: invalid invite token length (only got %d bytes)", n)
+ }
+
+ // hash the binary of the passed token
+ h := sha256.New()
+ h.Write(tokenBytes)
+ return fmt.Sprintf("%x", h.Sum(nil)), nil
+}
diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go
index 8fd9e6ee..bdb1275a 100644
--- a/web/handlers/admin/invites.go
+++ b/web/handlers/admin/invites.go
@@ -86,24 +86,12 @@ func (h invitesHandler) revokeConfirm(rw http.ResponseWriter, req *http.Request)
return nil, err
}
- // TODO: add GetByID to invite service
- var invite admindb.Invite
- list, err := h.db.List(req.Context())
+ invite, err := h.db.GetByID(req.Context(), id)
if err != nil {
- return nil, err
- }
-
- found := false
- for _, elem := range list {
- if elem.ID == id {
- invite = elem
- found = true
- break
+ if errors.Is(err, admindb.ErrNotFound) {
+ return nil, weberrors.ErrNotFound{What: "invite"}
}
- }
-
- if !found {
- return nil, weberrors.ErrNotFound{What: "invite"}
+ return nil, err
}
return map[string]interface{}{
From fd21dfc60a90413216c5acc8d4fd5e7d946ff6f9 Mon Sep 17 00:00:00 2001
From: Henry
Date: Fri, 5 Mar 2021 11:15:36 +0100
Subject: [PATCH 09/12] Various fixes
* fix accept and consumed view
* Apply suggestions from Alex' code review
* define admin.Databases options struct
* structify database parameters of web/handlers
---
admindb/sqlite/invites.go | 9 +++++
cmd/server/main.go | 14 ++++----
web/handlers/admin/app_test.go | 10 +++---
web/handlers/admin/handler.go | 21 +++++++-----
web/handlers/http.go | 51 ++++++++++++++++------------
web/handlers/invites.go | 21 ++++++++++--
web/handlers/setup_test.go | 14 ++++----
web/i18n/defaults/active.en.toml | 7 +++-
web/templates/admin/notice-edit.tmpl | 2 +-
web/templates/invite/accept.tmpl | 47 +++++++++++++++++++++----
web/user/helper.go | 5 ++-
web/user/testing.go | 2 +-
12 files changed, 143 insertions(+), 60 deletions(-)
diff --git a/admindb/sqlite/invites.go b/admindb/sqlite/invites.go
index a6790201..f671d159 100644
--- a/admindb/sqlite/invites.go
+++ b/admindb/sqlite/invites.go
@@ -155,6 +155,9 @@ func (i Invites) GetByToken(ctx context.Context, token string) (admindb.Invite,
qm.Load("CreatedByAuthFallback"),
).One(ctx, i.db)
if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return inv, admindb.ErrNotFound
+ }
return inv, err
}
@@ -174,6 +177,9 @@ func (i Invites) GetByID(ctx context.Context, id int64) (admindb.Invite, error)
qm.Load("CreatedByAuthFallback"),
).One(ctx, i.db)
if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return inv, admindb.ErrNotFound
+ }
return inv, err
}
@@ -225,6 +231,9 @@ func (i Invites) Revoke(ctx context.Context, id int64) error {
qm.Where("active = true AND id = ?", id),
).One(ctx, tx)
if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return admindb.ErrNotFound
+ }
return err
}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 65ab6d4f..6c66b58a 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -215,12 +215,14 @@ func runroomsrv() error {
repo.New(repoDir),
httpsDomain,
roomsrv.StateManager,
- db.AuthWithSSB,
- db.AuthFallback,
- db.AllowList,
- db.Invites,
- db.Notices,
- db.PinnedNotices,
+ handlers.Databases{
+ AuthWithSSB: db.AuthWithSSB,
+ AuthFallback: db.AuthFallback,
+ AllowList: db.AllowList,
+ Invites: db.Invites,
+ Notices: db.Notices,
+ PinnedNotices: db.PinnedNotices,
+ },
)
if err != nil {
return fmt.Errorf("failed to create HTTPdashboard handler: %w", err)
diff --git a/web/handlers/admin/app_test.go b/web/handlers/admin/app_test.go
index 4ccba985..f2ec299c 100644
--- a/web/handlers/admin/app_test.go
+++ b/web/handlers/admin/app_test.go
@@ -96,10 +96,12 @@ func newSession(t *testing.T) *testSession {
ts.Domain,
r,
ts.RoomState,
- ts.AllowListDB,
- ts.InvitesDB,
- ts.NoticeDB,
- ts.PinnedDB,
+ Databases{
+ AllowList: ts.AllowListDB,
+ Invites: ts.InvitesDB,
+ Notices: ts.NoticeDB,
+ PinnedNotices: ts.PinnedDB,
+ },
)
handler = user.MiddlewareForTests(ts.User)(handler)
diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go
index aa09b2a5..f4349d9f 100644
--- a/web/handlers/admin/handler.go
+++ b/web/handlers/admin/handler.go
@@ -32,16 +32,21 @@ var HTMLTemplates = []string{
"admin/notice-edit.tmpl",
}
+// Databases is an option struct that encapsualtes the required database services
+type Databases struct {
+ AllowList admindb.AllowListService
+ Invites admindb.InviteService
+ Notices admindb.NoticesService
+ PinnedNotices admindb.PinnedNoticesService
+}
+
// Handler supplies the elevated access pages to known users.
// It is not registering on the mux router like other pages to clean up the authorize flow.
func Handler(
domainName string,
r *render.Renderer,
roomState *roomstate.Manager,
- al admindb.AllowListService,
- is admindb.InviteService,
- ndb admindb.NoticesService,
- pdb admindb.PinnedNoticesService,
+ dbs Databases,
) http.Handler {
mux := &http.ServeMux{}
// TODO: configure 404 handler
@@ -59,7 +64,7 @@ func Handler(
var ah = allowListHandler{
r: r,
- al: al,
+ al: dbs.AllowList,
}
mux.HandleFunc("/members", r.HTML("admin/allow-list.tmpl", ah.overview))
mux.HandleFunc("/members/add", ah.add)
@@ -68,7 +73,7 @@ func Handler(
var ih = invitesHandler{
r: r,
- db: is,
+ db: dbs.Invites,
domainName: domainName,
}
@@ -79,8 +84,8 @@ func Handler(
var nh = noticeHandler{
r: r,
- noticeDB: ndb,
- pinnedDB: pdb,
+ noticeDB: dbs.Notices,
+ pinnedDB: dbs.PinnedNotices,
}
mux.HandleFunc("/notice/edit", r.HTML("admin/notice-edit.tmpl", nh.edit))
mux.HandleFunc("/notice/translation/draft", r.HTML("admin/notice-edit.tmpl", nh.draftTranslation))
diff --git a/web/handlers/http.go b/web/handlers/http.go
index 2e892112..901bc60f 100644
--- a/web/handlers/http.go
+++ b/web/handlers/http.go
@@ -39,18 +39,24 @@ var HTMLTemplates = []string{
"error.tmpl",
}
+// Databases is an options stuct for the required databases of the web handlers
+type Databases struct {
+ AuthWithSSB admindb.AuthWithSSBService
+ AuthFallback admindb.AuthFallbackService
+ AllowList admindb.AllowListService
+ Invites admindb.InviteService
+ Notices admindb.NoticesService
+ PinnedNotices admindb.PinnedNoticesService
+}
+
// New initializes the whole web stack for rooms, with all the sub-modules and routing.
func New(
logger logging.Interface,
repo repo.Interface,
domainName string,
roomState *roomstate.Manager,
- as admindb.AuthWithSSBService,
- fs admindb.AuthFallbackService,
- al admindb.AllowListService,
- is admindb.InviteService,
- ns admindb.NoticesService,
- ps admindb.PinnedNoticesService,
+ dbs Databases,
+
) (http.Handler, error) {
m := router.CompleteApp()
@@ -93,7 +99,7 @@ func New(
if !noticeName.Valid() {
return nil
}
- notice, err := ps.Get(r.Context(), noticeName, "en-GB")
+ notice, err := dbs.PinnedNotices.Get(r.Context(), noticeName, "en-GB")
if err != nil {
return nil
}
@@ -173,7 +179,7 @@ func New(
}, nil
})
- a, err := auth.NewHandler(fs,
+ a, err := auth.NewHandler(dbs.AuthFallback,
auth.SetStore(store),
auth.SetErrorHandler(authErrH),
auth.SetNotAuthorizedHandler(notAuthorizedH),
@@ -204,18 +210,21 @@ func New(
// hookup handlers to the router
roomsAuth.Handler(m, r, a)
- adminHandler := a.Authenticate(admin.Handler(
+ adminHandler := admin.Handler(
domainName,
r,
roomState,
- al,
- is,
- ns,
- ps))
- mainMux.Handle("/admin/", adminHandler)
+ admin.Databases{
+ AllowList: dbs.AllowList,
+ Invites: dbs.Invites,
+ Notices: dbs.Notices,
+ PinnedNotices: dbs.PinnedNotices,
+ },
+ )
+ mainMux.Handle("/admin/", a.Authenticate(adminHandler))
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
- notice, err := ps.Get(req.Context(), admindb.NoticeDescription, "en-GB")
+ notice, err := dbs.PinnedNotices.Get(req.Context(), admindb.NoticeDescription, "en-GB")
if err != nil {
return nil, fmt.Errorf("failed to find description: %w", err)
}
@@ -230,14 +239,14 @@ func New(
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
var nh = noticeHandler{
- notices: ns,
- pinned: ps,
+ notices: dbs.Notices,
+ pinned: dbs.PinnedNotices,
}
m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nh.list))
m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nh.show))
var ih = inviteHandler{
- invites: is,
+ invites: dbs.Invites,
}
m.Get(router.CompleteInviteAccept).Handler(r.HTML("invite/accept.tmpl", ih.acceptForm))
m.Get(router.CompleteInviteConsume).Handler(r.HTML("invite/consumed.tmpl", ih.consume))
@@ -255,7 +264,7 @@ func New(
// apply HTTP middleware
middlewares := []func(http.Handler) http.Handler{
logging.InjectHandler(logger),
- user.ContextInjecter(fs, a),
+ user.ContextInjecter(dbs.AuthFallback, a),
CSRF,
}
@@ -264,8 +273,8 @@ func New(
}
var finalHandler http.Handler = mainMux
- for _, mw := range middlewares {
- finalHandler = mw(finalHandler)
+ for _, applyMiddleware := range middlewares {
+ finalHandler = applyMiddleware(finalHandler)
}
return finalHandler, nil
diff --git a/web/handlers/invites.go b/web/handlers/invites.go
index 99ea66e8..0c6526e5 100644
--- a/web/handlers/invites.go
+++ b/web/handlers/invites.go
@@ -5,7 +5,9 @@ import (
"net/http"
"go.mindeco.de/http/render"
+ "go.mindeco.de/logging"
+ "github.com/go-kit/kit/log/level"
"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
@@ -20,7 +22,9 @@ type inviteHandler struct {
}
func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
- inv, err := h.invites.GetByToken(req.Context(), req.URL.Query().Get("token"))
+ token := req.URL.Query().Get("token")
+
+ inv, err := h.invites.GetByToken(req.Context(), token)
if err != nil {
if errors.Is(err, admindb.ErrNotFound) {
return nil, weberrors.ErrNotFound{What: "invite"}
@@ -29,7 +33,9 @@ func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (in
}
return map[string]interface{}{
- "Invite": inv,
+ "Token": token,
+ "Invite": inv,
+
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}
@@ -39,6 +45,8 @@ func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (inter
return nil, weberrors.ErrBadRequest{Where: "form data", Details: err}
}
+ alias := req.FormValue("alias")
+
token := req.FormValue("token")
newMember, err := refs.ParseFeedRef(req.FormValue("new_member"))
@@ -53,6 +61,15 @@ func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (inter
}
return nil, err
}
+ 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,
+ )
+ }
return map[string]interface{}{
"TunnelAddress": "pew pew",
diff --git a/web/handlers/setup_test.go b/web/handlers/setup_test.go
index 76093826..0f97b8e0 100644
--- a/web/handlers/setup_test.go
+++ b/web/handlers/setup_test.go
@@ -80,12 +80,14 @@ func setup(t *testing.T) *testSession {
testRepo,
"localhost",
ts.RoomState,
- ts.AuthDB,
- ts.AuthFallbackDB,
- ts.AllowListDB,
- ts.InvitesDB,
- ts.NoticeDB,
- ts.PinnedDB,
+ Databases{
+ AuthWithSSB: ts.AuthDB,
+ AuthFallback: ts.AuthFallbackDB,
+ AllowList: ts.AllowListDB,
+ Invites: ts.InvitesDB,
+ Notices: ts.NoticeDB,
+ PinnedNotices: ts.PinnedDB,
+ },
)
if err != nil {
t.Fatal("setup: handler init failed:", err)
diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml
index 3def3043..85683d06 100644
--- a/web/i18n/defaults/active.en.toml
+++ b/web/i18n/defaults/active.en.toml
@@ -2,6 +2,7 @@ GenericConfirm = "Yes"
GenericGoBack = "Back"
GenericSave = "Save"
GenericPreview = "Preview"
+GenericLanguage = "Language"
PageNotFound = "The requested page was not found."
@@ -45,9 +46,13 @@ NavAdminInvites = "Invites"
NavAdminNotices = "Notices"
InviteAccept = "Accept invite"
-InviteAcceptTitle = "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"
+
+InviteConsumedTitle = "Invite accepted!"
+InviteConsumedWelcome = "Even more elaborate message that the person is now a member of the room!"
NoticeEditTitle = "Edit Notice"
NoticeList = "Notices"
diff --git a/web/templates/admin/notice-edit.tmpl b/web/templates/admin/notice-edit.tmpl
index 47db1630..de040f67 100644
--- a/web/templates/admin/notice-edit.tmpl
+++ b/web/templates/admin/notice-edit.tmpl
@@ -38,7 +38,7 @@
>{{.Notice.Content}}
-
Language
+
{{i18n "GenericLanguage"}}
{{i18n "InviteAcceptWelcome"}}
+ >{{ i18n "InviteAcceptWelcome" }}
- {{if ne .AliasSuggestion ""}}
-
-
{{i18n "InviteAcceptAliasSuggestion"}} {{.AliasSuggestion}}
- {{end}}
+
+
+
+
-{{end}}
\ No newline at end of file
+{{ end }}
\ No newline at end of file
diff --git a/web/user/helper.go b/web/user/helper.go
index 33ce7997..f13aa011 100644
--- a/web/user/helper.go
+++ b/web/user/helper.go
@@ -13,7 +13,7 @@ type roomUserContextKeyType string
var roomUserContextKey roomUserContextKeyType = "ssb:room:httpcontext:user"
-// FromContext returns the user or nil of it's not logged in
+ // FromContext returns the user or nil if not logged in
func FromContext(ctx context.Context) *admindb.User {
v := ctx.Value(roomUserContextKey)
@@ -25,11 +25,10 @@ func FromContext(ctx context.Context) *admindb.User {
return user
}
-// ContextInjecter returns the middleware that injects the user value into the request context
+// ContextInjecter returns middleware for injecting a user id into the request context
func ContextInjecter(fs admindb.AuthFallbackService, a *auth.Handler) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-
v, err := a.AuthenticateRequest(req)
if err != nil {
next.ServeHTTP(w, req)
diff --git a/web/user/testing.go b/web/user/testing.go
index e204b625..1a6f8db4 100644
--- a/web/user/testing.go
+++ b/web/user/testing.go
@@ -8,7 +8,7 @@ import (
)
// MiddlewareForTests gives us a way to inject _test users_. It should not be used in production.
-// This is exists here because we need to use roomUserContextKey which shouldn't be exported either.
+// This is part of testing.go because we need to use roomUserContextKey, which shouldn't be exported either.
// TODO: could be protected with an extra build tag.
// (Sadly +build test does not exist https://github.com/golang/go/issues/21360 )
func MiddlewareForTests(user *admindb.User) func(http.Handler) http.Handler {
From 69452215579751570d02adec89dc769e54c04734 Mon Sep 17 00:00:00 2001
From: Henry
Date: Mon, 8 Mar 2021 14:06:45 +0100
Subject: [PATCH 10/12] check that invite.Consume adds to the allow list
---
admindb/sqlite/invites_test.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/admindb/sqlite/invites_test.go b/admindb/sqlite/invites_test.go
index c4adfd67..32dce164 100644
--- a/admindb/sqlite/invites_test.go
+++ b/admindb/sqlite/invites_test.go
@@ -68,11 +68,18 @@ func TestInvites(t *testing.T) {
r.Equal("bestie", lst[0].AliasSuggestion)
r.Equal(testUserName, lst[0].CreatedBy.Name)
+ nope := db.AllowList.HasFeed(ctx, newMember)
+ r.False(nope, "expected feed to not yet be on the allow list")
+
inv, err := db.Invites.Consume(ctx, tok, newMember)
r.NoError(err, "failed to consume the invite")
r.Equal(testUserName, inv.CreatedBy.Name)
r.NotEqualValues(0, inv.ID, "invite ID unset")
+ // consume also adds it to the allow list
+ yes := db.AllowList.HasFeed(ctx, newMember)
+ r.True(yes, "expected feed on the allow list")
+
lst, err = db.Invites.List(ctx)
r.NoError(err, "failed to get list of tokens post consume")
r.Len(lst, 0, "expected no active invites")
@@ -80,6 +87,7 @@ func TestInvites(t *testing.T) {
// can't use twice
_, err = db.Invites.Consume(ctx, tok, newMember)
r.Error(err, "failed to consume the invite")
+
})
t.Run("simple create but revoke before use", func(t *testing.T) {
From b21e2b2062b77fdc32944ed7634a0c83c2225e02 Mon Sep 17 00:00:00 2001
From: Henry
Date: Mon, 8 Mar 2021 15:47:30 +0100
Subject: [PATCH 11/12] More testing
* test for invite accept form rendering
* show placeholder if there is no suggested alias
* accept form and consume endpoint
---
web/handlers/http_test.go | 40 ++++++
web/handlers/invites_test.go | 222 +++++++++++++++++++++++++++++++
web/templates/invite/accept.tmpl | 21 ++-
3 files changed, 272 insertions(+), 11 deletions(-)
diff --git a/web/handlers/http_test.go b/web/handlers/http_test.go
index 89a5c44f..bb6b8366 100644
--- a/web/handlers/http_test.go
+++ b/web/handlers/http_test.go
@@ -4,6 +4,7 @@ package handlers
import (
"bytes"
+ "fmt"
"net/http"
"net/http/cookiejar"
"net/url"
@@ -220,6 +221,9 @@ func TestFallbackAuth(t *testing.T) {
}
// utils
+
+// TODO: we probably want to move all of these to web/testing or somesuch
+
type localizedElement struct {
Selector, Label string
}
@@ -239,3 +243,39 @@ func assertCSRFTokenPresent(t *testing.T, sel *goquery.Selection) {
a.True(ok, "csrf input has a type")
a.Equal("hidden", tipe, "wrong type on csrf field")
}
+
+type inputElement struct {
+ Name, Value, Type, Placeholder string
+}
+
+// assertInputsInForm checks a list of defined elements. It tries to find them by input[name=$name]
+// and then proceeds with asserting their value, type or placeholder (if the fields in inputElement are not "")
+func assertInputsInForm(t *testing.T, form *goquery.Selection, elems []inputElement) {
+ a := assert.New(t)
+ for _, e := range elems {
+
+ inputSelector := form.Find(fmt.Sprintf("input[name=%s]", e.Name))
+ ok := a.Equal(1, inputSelector.Length(), "expected to find input with name %s", e.Name)
+ if !ok {
+ continue
+ }
+
+ if e.Value != "" {
+ value, has := inputSelector.Attr("value")
+ a.True(has, "expected value attribute input[name=%s]", e.Name)
+ a.Equal(e.Value, value, "wrong value attribute on input[name=%s]", e.Name)
+ }
+
+ if e.Type != "" {
+ tipe, has := inputSelector.Attr("type")
+ a.True(has, "expected type attribute input[name=%s]", e.Name)
+ a.Equal(e.Type, tipe, "wrong type attribute on input[name=%s]", e.Name)
+ }
+
+ if e.Placeholder != "" {
+ tipe, has := inputSelector.Attr("placeholder")
+ a.True(has, "expected placeholder attribute input[name=%s]", e.Name)
+ a.Equal(e.Placeholder, tipe, "wrong placeholder attribute on input[name=%s]", e.Name)
+ }
+ }
+}
diff --git a/web/handlers/invites_test.go b/web/handlers/invites_test.go
index 5ac8282f..92d3a461 100644
--- a/web/handlers/invites_test.go
+++ b/web/handlers/invites_test.go
@@ -1 +1,223 @@
package handlers
+
+import (
+ "bytes"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+ "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/admindb"
+ "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"
+)
+
+func TestInviteShowAcceptForm(t *testing.T) {
+ ts := setup(t)
+
+ urlTo := web.NewURLTo(ts.Router)
+
+ t.Run("token doesnt exist", func(t *testing.T) {
+ a, r := assert.New(t), require.New(t)
+
+ testToken := "nonexistant-test-token"
+ acceptURL404 := urlTo(router.CompleteInviteAccept, "token", testToken)
+ r.NotNil(acceptURL404)
+
+ // prep the mocked db for http:404
+ ts.InvitesDB.GetByTokenReturns(admindb.Invite{}, admindb.ErrNotFound)
+
+ // request the form
+ acceptForm := acceptURL404.String()
+ t.Log(acceptForm)
+ doc, resp := ts.Client.GetHTML(acceptForm)
+ // 500 until https://github.com/ssb-ngi-pointer/go-ssb-room/issues/66 is fixed
+ a.Equal(http.StatusInternalServerError, resp.Code)
+
+ // check database calls
+ r.EqualValues(1, ts.InvitesDB.GetByTokenCallCount())
+ _, tokenFromArg := ts.InvitesDB.GetByTokenArgsForCall(0)
+ a.Equal(testToken, tokenFromArg)
+
+ // fix #66
+ // assertLocalized(t, doc, []localizedElement{
+ // {"#welcome", "AuthFallbackWelcome"},
+ // {"title", "AuthFallbackTitle"},
+ // })
+ gotErr := doc.Find("#errBody").Text()
+ wantErr := weberrors.ErrNotFound{What: "invite"}
+ a.EqualError(wantErr, gotErr)
+ })
+
+ wantNewMemberPlaceholder := "@ .ed25519"
+
+ t.Run("token DOES exist", func(t *testing.T) {
+ a, r := assert.New(t), require.New(t)
+
+ testToken := "existing-test-token"
+ validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken)
+ r.NotNil(validAcceptURL)
+
+ // prep the mocked db for http:200
+ fakeExistingInvite := admindb.Invite{
+ ID: 1234,
+ AliasSuggestion: "bestie",
+ }
+ ts.InvitesDB.GetByTokenReturns(fakeExistingInvite, nil)
+
+ // request the form
+ validAcceptForm := validAcceptURL.String()
+ t.Log(validAcceptForm)
+ doc, resp := ts.Client.GetHTML(validAcceptForm)
+ a.Equal(http.StatusOK, resp.Code)
+
+ // check database calls
+ r.EqualValues(2, ts.InvitesDB.GetByTokenCallCount())
+ _, tokenFromArg := ts.InvitesDB.GetByTokenArgsForCall(1)
+ a.Equal(testToken, tokenFromArg)
+
+ assertLocalized(t, doc, []localizedElement{
+ {"#welcome", "InviteAcceptWelcome"},
+ {"title", "InviteAcceptTitle"},
+ })
+
+ form := doc.Find("form#consume")
+ r.Equal(1, form.Length())
+
+ assertCSRFTokenPresent(t, form)
+
+ assertInputsInForm(t, form, []inputElement{
+ {Name: "token", Type: "hidden", Value: testToken},
+ {Name: "alias", Type: "text", Value: fakeExistingInvite.AliasSuggestion},
+ {Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},
+ })
+ })
+
+ t.Run("token DOES exist but has no suggested alias", func(t *testing.T) {
+ a, r := assert.New(t), require.New(t)
+
+ testToken := "existing-test-token-2"
+ validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken)
+ r.NotNil(validAcceptURL)
+
+ inviteWithNoAlias := admindb.Invite{ID: 4321}
+ ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil)
+
+ // request the form
+ validAcceptForm := validAcceptURL.String()
+ t.Log(validAcceptForm)
+ doc, resp := ts.Client.GetHTML(validAcceptForm)
+ a.Equal(http.StatusOK, resp.Code)
+
+ // check database calls
+ r.EqualValues(3, ts.InvitesDB.GetByTokenCallCount())
+ _, tokenFromArg := ts.InvitesDB.GetByTokenArgsForCall(2)
+ a.Equal(testToken, tokenFromArg)
+
+ assertLocalized(t, doc, []localizedElement{
+ {"#welcome", "InviteAcceptWelcome"},
+ {"title", "InviteAcceptTitle"},
+ })
+
+ form := doc.Find("form#consume")
+ r.Equal(1, form.Length())
+
+ assertCSRFTokenPresent(t, form)
+
+ assertInputsInForm(t, form, []inputElement{
+ {Name: "token", Type: "hidden", Value: testToken},
+ {Name: "alias", Type: "text", Placeholder: "you@this.room"},
+ {Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},
+ })
+ })
+}
+
+func TestInviteConsumeInvite(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)
+ r.NotNil(validAcceptURL)
+ validAcceptURL.Host = "localhost"
+ validAcceptURL.Scheme = "https"
+
+ inviteWithNoAlias := admindb.Invite{ID: 4321}
+ ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil)
+
+ // request the form (for a valid csrf token)
+ validAcceptForm := validAcceptURL.String()
+ t.Log(validAcceptForm)
+ doc, resp := ts.Client.GetHTML(validAcceptForm)
+ a.Equal(http.StatusOK, resp.Code)
+
+ // we need a functional jar to unpack the Set-Cookie response for the csrf token
+ jar, err := cookiejar.New(nil)
+ r.NoError(err)
+
+ // update the jar
+ csrfCookie := resp.Result().Cookies()
+ a.Len(csrfCookie, 1, "should have one cookie for CSRF protection validation")
+ jar.SetCookies(validAcceptURL, csrfCookie)
+
+ // get the corresponding token from the page
+ csrfTokenElem := doc.Find("input[name='gorilla.csrf.Token']")
+ a.Equal(1, csrfTokenElem.Length())
+ csrfName, has := csrfTokenElem.Attr("name")
+ a.True(has, "should have a name attribute")
+ csrfValue, has := csrfTokenElem.Attr("value")
+ a.True(has, "should have value attribute")
+
+ // create the consume request
+ testNewMember := refs.FeedRef{
+ ID: bytes.Repeat([]byte{1}, 32),
+ Algo: refs.RefAlgoFeedSSB1,
+ }
+ consumeVals := url.Values{
+ "token": []string{testToken},
+ "new_member": []string{testNewMember.Ref()},
+
+ csrfName: []string{csrfValue},
+ }
+
+ // construct the consume endpoint url
+ consumeInviteURL, err := ts.Router.Get(router.CompleteInviteConsume).URL()
+ r.Nil(err)
+ consumeInviteURL.Host = "localhost"
+ consumeInviteURL.Scheme = "https"
+
+ // construct the header with Referer and Cookie
+ var csrfCookieHeader = http.Header(map[string][]string{})
+ csrfCookieHeader.Set("Referer", "https://localhost")
+ cs := jar.Cookies(consumeInviteURL)
+ r.Len(cs, 1, "expecting one cookie for csrf")
+ theCookie := cs[0].String()
+ a.NotEqual("", theCookie, "should have a new cookie")
+ csrfCookieHeader.Set("Cookie", theCookie)
+ ts.Client.SetHeaders(csrfCookieHeader)
+
+ // prepare the mock
+ ts.InvitesDB.ConsumeReturns(inviteWithNoAlias, nil)
+
+ // send the POST
+ resp = ts.Client.PostForm(consumeInviteURL.String(), consumeVals)
+ 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))
+
+ consumedDoc, err := goquery.NewDocumentFromReader(resp.Body)
+ r.NoError(err)
+ t.Log(consumedDoc.Find("body").Text())
+}
diff --git a/web/templates/invite/accept.tmpl b/web/templates/invite/accept.tmpl
index 91e7f4a5..7b3fec3a 100644
--- a/web/templates/invite/accept.tmpl
+++ b/web/templates/invite/accept.tmpl
@@ -7,11 +7,7 @@
class="text-center"
>{{ i18n "InviteAcceptWelcome" }}
-
-
-
-
-
+
- {{ end }}
+ {{else}}
+ placeholder="you@this.room"
+ {{ end }}
+ >
Date: Tue, 9 Mar 2021 18:27:44 +0100
Subject: [PATCH 12/12] add room v1 "invite" until we have ssb-uri finalized
---
cmd/server/main.go | 30 ++++++++++++++++++++++++++++--
web/handlers/http.go | 18 ++++++++++++++++--
web/handlers/invites.go | 12 +++++++++++-
web/handlers/invites_test.go | 11 ++++++++++-
web/handlers/setup_test.go | 12 +++++++++++-
web/templates/invite/consumed.tmpl | 3 ++-
6 files changed, 78 insertions(+), 8 deletions(-)
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 6c66b58a..448188d3 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -127,12 +127,33 @@ func runroomsrv() error {
return fmt.Errorf("https-domain can't be empty. See '%s -h' for a full list of options", os.Args[0])
}
+ // validate listen addresses to bail out on invalid flag input before doing anything else
+ _, muxrpcPortStr, err := net.SplitHostPort(listenAddrShsMux)
+ if err != nil {
+ return fmt.Errorf("invalid muxrpc listener: %w", err)
+ }
+
+ portMUXRPC, err := net.LookupPort("tcp", muxrpcPortStr)
+ if err != nil {
+ return fmt.Errorf("invalid tcp port for muxrpc listener: %w", err)
+ }
+
+ _, portHTTPStr, err := net.SplitHostPort(listenAddrHTTP)
+ if err != nil {
+ return fmt.Errorf("invalid http listener: %w", err)
+ }
+
+ portHTTP, err := net.LookupPort("tcp", portHTTPStr)
+ if err != nil {
+ return fmt.Errorf("invalid tcp port for muxrpc listener: %w", err)
+ }
+
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ak, err := base64.StdEncoding.DecodeString(appKey)
if err != nil {
- return fmt.Errorf("application key: %w", err)
+ return fmt.Errorf("secret-handshake appkey is invalid base64: %w", err)
}
opts := []roomsrv.Option{
@@ -213,7 +234,12 @@ func runroomsrv() error {
dashboardH, err := handlers.New(
kitlog.With(log, "package", "web"),
repo.New(repoDir),
- httpsDomain,
+ handlers.NetworkInfo{
+ Domain: httpsDomain,
+ PortHTTPS: uint(portHTTP),
+ PortMUXRPC: uint(portMUXRPC),
+ PubKey: roomsrv.Whoami().PubKey(),
+ },
roomsrv.StateManager,
handlers.Databases{
AuthWithSSB: db.AuthWithSSB,
diff --git a/web/handlers/http.go b/web/handlers/http.go
index 901bc60f..aa759b49 100644
--- a/web/handlers/http.go
+++ b/web/handlers/http.go
@@ -17,6 +17,7 @@ import (
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
"go.mindeco.de/logging"
+ "golang.org/x/crypto/ed25519"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
@@ -49,11 +50,21 @@ type Databases struct {
PinnedNotices admindb.PinnedNoticesService
}
+// NetworkInfo encapsulates the domain name of the room, it's ssb/secret-handshake public key and the HTTP and MUXRPC TCP ports.
+type NetworkInfo struct {
+ PortMUXRPC uint
+ PortHTTPS uint // 0 assumes default (443)
+
+ PubKey ed25519.PublicKey
+
+ Domain string
+}
+
// New initializes the whole web stack for rooms, with all the sub-modules and routing.
func New(
logger logging.Interface,
repo repo.Interface,
- domainName string,
+ netInfo NetworkInfo,
roomState *roomstate.Manager,
dbs Databases,
@@ -211,7 +222,7 @@ func New(
roomsAuth.Handler(m, r, a)
adminHandler := admin.Handler(
- domainName,
+ netInfo.Domain,
r,
roomState,
admin.Databases{
@@ -247,6 +258,9 @@ func New(
var ih = inviteHandler{
invites: dbs.Invites,
+
+ roomPubKey: netInfo.PubKey,
+ muxrpcHostAndPort: fmt.Sprintf("%s:%d", netInfo.Domain, netInfo.PortMUXRPC),
}
m.Get(router.CompleteInviteAccept).Handler(r.HTML("invite/accept.tmpl", ih.acceptForm))
m.Get(router.CompleteInviteConsume).Handler(r.HTML("invite/consumed.tmpl", ih.consume))
diff --git a/web/handlers/invites.go b/web/handlers/invites.go
index 0c6526e5..2fcc93bc 100644
--- a/web/handlers/invites.go
+++ b/web/handlers/invites.go
@@ -1,11 +1,14 @@
package handlers
import (
+ "encoding/base64"
"errors"
+ "fmt"
"net/http"
"go.mindeco.de/http/render"
"go.mindeco.de/logging"
+ "golang.org/x/crypto/ed25519"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/csrf"
@@ -19,6 +22,9 @@ type inviteHandler struct {
invites admindb.InviteService
alaises admindb.AliasService
+
+ muxrpcHostAndPort string
+ roomPubKey ed25519.PublicKey
}
func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
@@ -71,7 +77,11 @@ func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (inter
)
}
+ // 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{}{
- "TunnelAddress": "pew pew",
+ "RoomAddress": roomAddr,
}, nil
}
diff --git a/web/handlers/invites_test.go b/web/handlers/invites_test.go
index 92d3a461..eb828122 100644
--- a/web/handlers/invites_test.go
+++ b/web/handlers/invites_test.go
@@ -2,9 +2,11 @@ package handlers
import (
"bytes"
+ "encoding/base64"
"net/http"
"net/http/cookiejar"
"net/url"
+ "strings"
"testing"
"github.com/PuerkitoBio/goquery"
@@ -219,5 +221,12 @@ func TestInviteConsumeInvite(t *testing.T) {
consumedDoc, err := goquery.NewDocumentFromReader(resp.Body)
r.NoError(err)
- t.Log(consumedDoc.Find("body").Text())
+
+ gotRA := consumedDoc.Find("#room-address").Text()
+
+ // 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.PubKey)), "public key missing? %s", gotRA)
+ a.True(strings.HasSuffix(gotRA, ":SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24="), "magic suffix missing: %s", gotRA)
+
}
diff --git a/web/handlers/setup_test.go b/web/handlers/setup_test.go
index 0f97b8e0..6c427290 100644
--- a/web/handlers/setup_test.go
+++ b/web/handlers/setup_test.go
@@ -39,6 +39,8 @@ type testSession struct {
NoticeDB *mockdb.FakeNoticesService
RoomState *roomstate.Manager
+
+ NetworkInfo NetworkInfo
}
var testI18N = justTheKeys()
@@ -69,6 +71,14 @@ func setup(t *testing.T) *testSession {
ts.PinnedDB.GetReturns(defaultNotice, nil)
ts.NoticeDB = new(mockdb.FakeNoticesService)
+ ts.NetworkInfo = NetworkInfo{
+ Domain: "localhost",
+ PortMUXRPC: 8008,
+ PortHTTPS: 443,
+
+ PubKey: bytes.Repeat([]byte("test"), 8),
+ }
+
log, _ := logtest.KitLogger("complete", t)
ctx := context.TODO()
ts.RoomState = roomstate.NewManager(ctx, log)
@@ -78,7 +88,7 @@ func setup(t *testing.T) *testSession {
h, err := New(
log,
testRepo,
- "localhost",
+ ts.NetworkInfo,
ts.RoomState,
Databases{
AuthWithSSB: ts.AuthDB,
diff --git a/web/templates/invite/consumed.tmpl b/web/templates/invite/consumed.tmpl
index cefcb3cf..240b4e21 100644
--- a/web/templates/invite/consumed.tmpl
+++ b/web/templates/invite/consumed.tmpl
@@ -7,6 +7,7 @@
class="text-center"
>{{i18n "InviteConsumedWelcome"}}
- TODO: present tunnel address and ssb uri redirect
+ TODO: this is just a room v1 invite . present tunnel address and ssb uri redirect
+ {{.RoomAddress}}
{{end}}
\ No newline at end of file