From 50e4ebbaca5ecfe9db802af3b93ceed56de099c9 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 12 Mar 2021 14:42:30 +0100 Subject: [PATCH] aliases: add overview and revoke handlers --- cmd/server/main.go | 1 + web/handlers/admin/aliases.go | 97 ++++++++++++++ web/handlers/admin/aliases_test.go | 122 ++++++++++++++++++ web/handlers/admin/allow_list.go | 2 + web/handlers/admin/app_test.go | 3 + web/handlers/admin/handler.go | 22 +++- web/handlers/http.go | 10 +- web/i18n/defaults/active.en.toml | 4 + web/router/admin.go | 8 ++ .../admin/aliases-revoke-confirm.tmpl | 31 +++++ web/templates/admin/aliases.tmpl | 69 ++++++++++ web/templates/menu.tmpl | 9 ++ 12 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 web/handlers/admin/aliases.go create mode 100644 web/handlers/admin/aliases_test.go create mode 100644 web/templates/admin/aliases-revoke-confirm.tmpl create mode 100644 web/templates/admin/aliases.tmpl diff --git a/cmd/server/main.go b/cmd/server/main.go index dab5f15..05a438b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -245,6 +245,7 @@ func runroomsrv() error { }, roomsrv.StateManager, handlers.Databases{ + Aliases: db.Aliases, AuthWithSSB: db.AuthWithSSB, AuthFallback: db.AuthFallback, AllowList: db.AllowList, diff --git a/web/handlers/admin/aliases.go b/web/handlers/admin/aliases.go new file mode 100644 index 0000000..81105eb --- /dev/null +++ b/web/handlers/admin/aliases.go @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT + +package admin + +import ( + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/gorilla/csrf" + "go.mindeco.de/http/render" + + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors" +) + +type aliasesHandler struct { + r *render.Renderer + + db roomdb.AliasService +} + +const redirectToAliases = "/admin/aliases" + +func (h aliasesHandler) 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] + } + + pageData, err := paginate(lst, len(lst), req.URL.Query()) + if err != nil { + return nil, err + } + + return pageData, nil +} + +func (h aliasesHandler) revokeConfirm(rw http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected GET request")} + } + + id, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64) + if err != nil { + err = weberrors.ErrBadRequest{Where: "ID", Details: err} + return nil, err + } + + entry, err := h.db.GetByID(req.Context(), id) + if err != nil { + if errors.Is(err, roomdb.ErrNotFound) { + http.Redirect(rw, req, redirectToAliases, http.StatusFound) + return nil, ErrRedirected + } + return nil, err + } + + return map[string]interface{}{ + "Entry": entry, + csrf.TemplateTag: csrf.TemplateField(req), + }, nil +} + +func (h aliasesHandler) revoke(rw http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST request")} + h.r.Error(rw, req, http.StatusMethodNotAllowed, err) + return + } + + err := req.ParseForm() + if err != nil { + err = weberrors.ErrBadRequest{Where: "Form data", Details: err} + http.Redirect(rw, req, redirectToAliases, http.StatusFound) + return + } + + status := http.StatusFound + err = h.db.Revoke(req.Context(), req.FormValue("name")) + if err != nil { + if !errors.Is(err, roomdb.ErrNotFound) { + + h.r.Error(rw, req, http.StatusInternalServerError, err) + return + } + status = http.StatusNotFound + } + + http.Redirect(rw, req, redirectToAliases, status) +} diff --git a/web/handlers/admin/aliases_test.go b/web/handlers/admin/aliases_test.go new file mode 100644 index 0000000..f51d47b --- /dev/null +++ b/web/handlers/admin/aliases_test.go @@ -0,0 +1,122 @@ +package admin + +import ( + "bytes" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + "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/webassert" + refs "go.mindeco.de/ssb-refs" +) + +func TestAliasesOverview(t *testing.T) { + ts := newSession(t) + a := assert.New(t) + + lst := []roomdb.Alias{ + {ID: 1, Name: "alice", Feed: refs.FeedRef{ID: bytes.Repeat([]byte{0}, 32), Algo: "fake"}}, + {ID: 2, Name: "bob", Feed: refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: "test"}}, + {ID: 3, Name: "cleo", Feed: refs.FeedRef{ID: bytes.Repeat([]byte("acab"), 8), Algo: "true"}}, + } + ts.Aliases.ListReturns(lst, nil) + + html, resp := ts.Client.GetHTML("/aliases") + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + + webassert.Localized(t, html, []webassert.LocalizedElement{ + {"#welcome", "AdminAliasesWelcome"}, + {"title", "AdminAliasesTitle"}, + {"#aliasCount", "ListCountPlural"}, + }) + + a.EqualValues(html.Find("#theList li").Length(), 3) + + lst = []roomdb.Alias{ + {ID: 666, Name: "dave", Feed: refs.FeedRef{ID: bytes.Repeat([]byte{1}, 32), Algo: "one"}}, + } + ts.Aliases.ListReturns(lst, nil) + + html, resp = ts.Client.GetHTML("/aliases") + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + + webassert.Localized(t, html, []webassert.LocalizedElement{ + {"#welcome", "AdminAliasesWelcome"}, + {"title", "AdminAliasesTitle"}, + {"#aliasCount", "ListCountSingular"}, + }) + + elems := html.Find("#theList li") + a.EqualValues(elems.Length(), 1) + + // check for link to Revoke confirm link + link, yes := elems.ContentsFiltered("a").Attr("href") + a.True(yes, "a-tag has href attribute") + a.Equal("/admin/aliases/revoke/confirm?id=666", link) +} + +func TestAliasesRevokeConfirmation(t *testing.T) { + ts := newSession(t) + a := assert.New(t) + + testKey, err := refs.ParseFeedRef("@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519") + a.NoError(err) + testEntry := roomdb.Alias{ID: 666, Name: "the-test-name", Feed: *testKey} + ts.Aliases.GetByIDReturns(testEntry, nil) + + urlTo := web.NewURLTo(ts.Router) + urlRevokeConfirm := urlTo(router.AdminAliasesRevokeConfirm, "id", 3) + + html, resp := ts.Client.GetHTML(urlRevokeConfirm.String()) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + + a.Equal(testKey.Ref(), html.Find("pre#verify").Text(), "has the key for verification") + + form := html.Find("form#confirm") + + method, ok := form.Attr("method") + a.True(ok, "form has method set") + a.Equal("POST", method) + + action, ok := form.Attr("action") + a.True(ok, "form has action set") + + addURL, err := ts.Router.Get(router.AdminAliasesRevoke).URL() + a.NoError(err) + + a.Equal(addURL.String(), action) + + webassert.InputsInForm(t, form, []webassert.InputElement{ + {Name: "name", Type: "hidden", Value: testEntry.Name}, + }) +} + +func TestAliasesRevoke(t *testing.T) { + ts := newSession(t) + a := assert.New(t) + + urlTo := web.NewURLTo(ts.Router) + urlRevoke := urlTo(router.AdminAliasesRevoke) + + ts.Aliases.RevokeReturns(nil) + + addVals := url.Values{"name": []string{"the-name"}} + rec := ts.Client.PostForm(urlRevoke.String(), addVals) + a.Equal(http.StatusFound, rec.Code) + + a.Equal(1, ts.Aliases.RevokeCallCount()) + _, theName := ts.Aliases.RevokeArgsForCall(0) + a.EqualValues("the-name", theName) + + // now for unknown ID + ts.Aliases.RevokeReturns(roomdb.ErrNotFound) + addVals = url.Values{"name": []string{"nope"}} + rec = ts.Client.PostForm(urlRevoke.String(), addVals) + a.Equal(http.StatusNotFound, rec.Code) + //TODO: update redirect code with flash errors +} diff --git a/web/handlers/admin/allow_list.go b/web/handlers/admin/allow_list.go index f610ae8..feb78d0 100644 --- a/web/handlers/admin/allow_list.go +++ b/web/handlers/admin/allow_list.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + package admin import ( diff --git a/web/handlers/admin/app_test.go b/web/handlers/admin/app_test.go index 832c156..006575d 100644 --- a/web/handlers/admin/app_test.go +++ b/web/handlers/admin/app_test.go @@ -27,6 +27,7 @@ type testSession struct { Client *tester.Tester Router *mux.Router + Aliases *mockdb.FakeAliasService AllowListDB *mockdb.FakeAllowListService PinnedDB *mockdb.FakePinnedNoticesService NoticeDB *mockdb.FakeNoticesService @@ -43,6 +44,7 @@ func newSession(t *testing.T) *testSession { var ts testSession // fake dbs + ts.Aliases = new(mockdb.FakeAliasService) ts.AllowListDB = new(mockdb.FakeAllowListService) ts.PinnedDB = new(mockdb.FakePinnedNoticesService) ts.NoticeDB = new(mockdb.FakeNoticesService) @@ -97,6 +99,7 @@ func newSession(t *testing.T) *testSession { r, ts.RoomState, Databases{ + Aliases: ts.Aliases, AllowList: ts.AllowListDB, Invites: ts.InvitesDB, Notices: ts.NoticeDB, diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go index 719a4a4..05e2629 100644 --- a/web/handlers/admin/handler.go +++ b/web/handlers/admin/handler.go @@ -22,6 +22,9 @@ var HTMLTemplates = []string{ "admin/dashboard.tmpl", "admin/menu.tmpl", + "admin/aliases.tmpl", + "admin/aliases-revoke-confirm.tmpl", + "admin/allow-list.tmpl", "admin/allow-list-remove-confirm.tmpl", @@ -34,6 +37,7 @@ var HTMLTemplates = []string{ // Databases is an option struct that encapsualtes the required database services type Databases struct { + Aliases roomdb.AliasService AllowList roomdb.AllowListService Invites roomdb.InviteService Notices roomdb.NoticesService @@ -62,14 +66,22 @@ func Handler( return map[string]interface{}{}, nil })) - var ah = allowListHandler{ + var ah = aliasesHandler{ + r: r, + db: dbs.Aliases, + } + mux.HandleFunc("/aliases", r.HTML("admin/aliases.tmpl", ah.overview)) + mux.HandleFunc("/aliases/revoke/confirm", r.HTML("admin/aliases-revoke-confirm.tmpl", ah.revokeConfirm)) + mux.HandleFunc("/aliases/revoke", ah.revoke) + + var mh = allowListHandler{ r: r, al: dbs.AllowList, } - 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)) - mux.HandleFunc("/members/remove", ah.remove) + mux.HandleFunc("/members", r.HTML("admin/allow-list.tmpl", mh.overview)) + mux.HandleFunc("/members/add", mh.add) + mux.HandleFunc("/members/remove/confirm", r.HTML("admin/allow-list-remove-confirm.tmpl", mh.removeConfirm)) + mux.HandleFunc("/members/remove", mh.remove) var ih = invitesHandler{ r: r, diff --git a/web/handlers/http.go b/web/handlers/http.go index c60c48e..5f62b5b 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -19,8 +19,8 @@ import ( "go.mindeco.de/logging" "golang.org/x/crypto/ed25519" - "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/internal/repo" + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/roomstate" "github.com/ssb-ngi-pointer/go-ssb-room/web" "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/admin" @@ -42,6 +42,7 @@ var HTMLTemplates = []string{ // Databases is an options stuct for the required databases of the web handlers type Databases struct { + Aliases roomdb.AliasService AuthWithSSB roomdb.AuthWithSSBService AuthFallback roomdb.AuthFallbackService AllowList roomdb.AllowListService @@ -97,7 +98,11 @@ func New( }), render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} { return func(routeName string) bool { - url, err := router.CompleteApp().Get(routeName).URLPath() + route := router.CompleteApp().Get(routeName) + if route == nil { + return false + } + url, err := route.URLPath() if err != nil { return false } @@ -226,6 +231,7 @@ func New( r, roomState, admin.Databases{ + Aliases: dbs.Aliases, AllowList: dbs.AllowList, Invites: dbs.Invites, Notices: dbs.Notices, diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index 85683d0..31d1c9e 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -17,6 +17,10 @@ AuthSignOut = "Sign out" AdminDashboardWelcome = "Welcome to your dashboard" AdminDashboardTitle = "Room Admin Dashboard" +AdminAliasesTitle = "Aliases" +AdminAliasesWelcome = "Here you can see and revoke the registerd aliases of this room." +AdminAliasesRevoke = "Revoke" + AdminAllowListTitle = "Members" AdminAllowListWelcome = "Here you can see all the members of the room and ways to add new ones (by their SSB ID) or remove exising ones." AdminAllowListAdd = "Add" diff --git a/web/router/admin.go b/web/router/admin.go index 0848875..488b4b2 100644 --- a/web/router/admin.go +++ b/web/router/admin.go @@ -9,6 +9,10 @@ const ( AdminDashboard = "admin:dashboard" AdminMenu = "admin:menu" + AdminAliasesOverview = "admin:aliases:overview" + AdminAliasesRevokeConfirm = "admin:aliases:revoke:confirm" + AdminAliasesRevoke = "admin:aliases:revoke" + AdminAllowListOverview = "admin:allow-list:overview" AdminAllowListAdd = "admin:allow-list:add" AdminAllowListRemoveConfirm = "admin:allow-list:remove:confirm" @@ -34,6 +38,10 @@ func Admin(m *mux.Router) *mux.Router { m.Path("/dashboard").Methods("GET").Name(AdminDashboard) m.Path("/menu").Methods("GET").Name(AdminMenu) + m.Path("/aliases").Methods("GET").Name(AdminAliasesOverview) + m.Path("/aliases/revoke/confirm").Methods("GET").Name(AdminAliasesRevokeConfirm) + m.Path("/aliases/revoke").Methods("POST").Name(AdminAliasesRevoke) + m.Path("/members").Methods("GET").Name(AdminAllowListOverview) m.Path("/members/add").Methods("POST").Name(AdminAllowListAdd) m.Path("/members/remove/confirm").Methods("GET").Name(AdminAllowListRemoveConfirm) diff --git a/web/templates/admin/aliases-revoke-confirm.tmpl b/web/templates/admin/aliases-revoke-confirm.tmpl new file mode 100644 index 0000000..1b76ec4 --- /dev/null +++ b/web/templates/admin/aliases-revoke-confirm.tmpl @@ -0,0 +1,31 @@ +{{ define "title" }}{{i18n "AdminAliasesRevokeConfirmTitle"}}{{ end }} +{{ define "content" }} +
+ + {{i18n "AdminAliasesRevokeConfirmWelcome"}} + +
{{.Entry.Feed.Ref}}
+ +
+ {{ .csrfField }} + +
+ {{i18n "GenericGoBack"}} + + +
+
+
+{{end}} \ No newline at end of file diff --git a/web/templates/admin/aliases.tmpl b/web/templates/admin/aliases.tmpl new file mode 100644 index 0000000..5ffd7b2 --- /dev/null +++ b/web/templates/admin/aliases.tmpl @@ -0,0 +1,69 @@ +{{ define "title" }}{{i18n "AdminAliasesTitle"}}{{ end }} +{{ define "content" }} +

{{i18n "AdminAliasesTitle"}}

+ +

{{i18n "AdminAliasesWelcome"}}

+ +

{{i18npl "ListCount" .Count}}

+ + + + {{$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 3936431..d1b5b98 100644 --- a/web/templates/menu.tmpl +++ b/web/templates/menu.tmpl @@ -18,6 +18,15 @@ {{i18n "NavAdminDashboard"}} + + + + {{i18n "AdminAliasesTitle"}} + +