add denied templates and handlers

copy of previous allow-list stuff
This commit is contained in:
Henry 2021-03-19 13:30:00 +01:00
parent 36d46a8576
commit 1b6f7f5006
7 changed files with 502 additions and 11 deletions

View File

@ -0,0 +1,142 @@
// SPDX-License-Identifier: MIT
package admin
import (
"errors"
"fmt"
"net/http"
"strconv"
"go.mindeco.de/http/render"
refs "go.mindeco.de/ssb-refs"
"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
)
type deniedKeysHandler struct {
r *render.Renderer
db roomdb.DeniedKeysService
}
const redirectToDeniedKeys = "/admin/denied"
func (h deniedKeysHandler) add(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
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
}
newEntry := req.Form.Get("pub_key")
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
return
}
// can be empty
comment := req.Form.Get("comment")
err = h.db.Add(req.Context(), *newEntryParsed, comment)
if err != nil {
code := http.StatusInternalServerError
var aa roomdb.ErrAlreadyAdded
if errors.As(err, &aa) {
code = http.StatusBadRequest
// TODO: localized error pages
// h.r.Error(w, req, http.StatusBadRequest, weberrors.Localize())
// return
}
h.r.Error(w, req, code, err)
return
}
http.Redirect(w, req, redirectToDeniedKeys, http.StatusFound)
}
func (h deniedKeysHandler) 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
}
pageData[csrf.TemplateTag] = csrf.TemplateField(req)
return pageData, nil
}
// 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 deniedKeysHandler) 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}
return nil, err
}
entry, err := h.db.GetByID(req.Context(), id)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusFound)
return nil, ErrRedirected
}
return nil, err
}
return map[string]interface{}{
"Entry": entry,
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}
func (h deniedKeysHandler) remove(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, redirectToDeniedKeys, http.StatusFound)
return
}
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, redirectToDeniedKeys, http.StatusFound)
return
}
status := http.StatusFound
err = h.db.RemoveID(req.Context(), id)
if err != nil {
if !errors.Is(err, roomdb.ErrNotFound) {
// TODO "flash" errors
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
status = http.StatusNotFound
}
http.Redirect(rw, req, redirectToDeniedKeys, status)
}

View File

@ -0,0 +1,216 @@
// SPDX-License-Identifier: MIT
package admin
import (
"bytes"
"net/http"
"net/url"
"strings"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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 TestDeniedKeysEmpty(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
url, err := ts.Router.Get(router.AdminDeniedKeysOverview).URL()
a.Nil(err)
html, resp := ts.Client.GetHTML(url.String())
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
{"#welcome", "AdminDeniedKeysWelcome"},
{"title", "AdminDeniedKeysTitle"},
{"#DeniedKeysCount", "MemberCountPlural"},
})
}
func TestDeniedKeysAdd(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
listURL, err := ts.Router.Get(router.AdminDeniedKeysOverview).URL()
a.NoError(err)
html, resp := ts.Client.GetHTML(listURL.String())
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
formSelection := html.Find("form#add-entry")
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.AdminDeniedKeysAdd).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
webassert.ElementsInForm(t, formSelection, []webassert.FormElement{
{Name: "pub_key", Type: "text"},
{Name: "comment", Type: "text"},
})
newKey := "@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519"
addVals := url.Values{
"comment": []string{"some comment"},
// just any key that looks valid
"pub_key": []string{newKey},
}
rec := ts.Client.PostForm(addURL.String(), addVals)
a.Equal(http.StatusFound, rec.Code)
a.Equal(1, ts.DeniedKeysDB.AddCallCount())
_, addedKey, addedComment := ts.DeniedKeysDB.AddArgsForCall(0)
a.Equal(newKey, addedKey.Ref())
a.Equal("some comment", addedComment)
}
func TestDeniedKeysDontAddInvalid(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
r := require.New(t)
addURL, err := ts.Router.Get(router.AdminDeniedKeysAdd).URL()
a.NoError(err)
newKey := "@some-garbage"
addVals := url.Values{
"pub_key": []string{newKey},
}
rec := ts.Client.PostForm(addURL.String(), addVals)
a.Equal(http.StatusBadRequest, rec.Code)
a.Equal(0, ts.DeniedKeysDB.AddCallCount())
doc, err := goquery.NewDocumentFromReader(rec.Body)
r.NoError(err)
expErr := `bad request: feedRef: couldn't parse "@some-garbage"`
gotMsg := doc.Find("#errBody").Text()
if !a.True(strings.HasPrefix(gotMsg, expErr), "did not find errBody") {
t.Log(gotMsg)
}
}
func TestDeniedKeys(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
lst := []roomdb.ListEntry{
{ID: 1, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte{0}, 32), Algo: "fake"}},
{ID: 2, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: "test"}},
{ID: 3, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte("acab"), 8), Algo: "true"}},
}
ts.DeniedKeysDB.ListReturns(lst, nil)
html, resp := ts.Client.GetHTML("/members")
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
{"#welcome", "AdminDeniedKeysWelcome"},
{"title", "AdminDeniedKeysTitle"},
{"#DeniedKeysCount", "MemberCountPlural"},
})
a.EqualValues(html.Find("#theList li").Length(), 3)
lst = []roomdb.ListEntry{
{ID: 666, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte{1}, 32), Algo: "one"}},
}
ts.DeniedKeysDB.ListReturns(lst, nil)
html, resp = ts.Client.GetHTML("/members")
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
{"#welcome", "AdminDeniedKeysWelcome"},
{"title", "AdminDeniedKeysTitle"},
{"#DeniedKeysCount", "MemberCountSingular"},
})
elems := html.Find("#theList li")
a.EqualValues(elems.Length(), 1)
// check for link to remove confirm link
link, yes := elems.ContentsFiltered("a").Attr("href")
a.True(yes, "a-tag has href attribute")
a.Equal("/admin/members/remove/confirm?id=666", link)
}
func TestDeniedKeysRemoveConfirmation(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
testKey, err := refs.ParseFeedRef("@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519")
a.NoError(err)
testEntry := roomdb.ListEntry{ID: 666, PubKey: *testKey}
ts.DeniedKeysDB.GetByIDReturns(testEntry, nil)
urlTo := web.NewURLTo(ts.Router)
urlRemoveConfirm := urlTo(router.AdminDeniedKeysRemoveConfirm, "id", 3)
html, resp := ts.Client.GetHTML(urlRemoveConfirm.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.AdminDeniedKeysRemove).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "id", Type: "hidden", Value: "666"},
})
}
func TestDeniedKeysRemove(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
urlTo := web.NewURLTo(ts.Router)
urlRemove := urlTo(router.AdminDeniedKeysRemove)
ts.DeniedKeysDB.RemoveIDReturns(nil)
addVals := url.Values{"id": []string{"666"}}
rec := ts.Client.PostForm(urlRemove.String(), addVals)
a.Equal(http.StatusFound, rec.Code)
a.Equal(1, ts.DeniedKeysDB.RemoveIDCallCount())
_, theID := ts.DeniedKeysDB.RemoveIDArgsForCall(0)
a.EqualValues(666, theID)
// now for unknown ID
ts.DeniedKeysDB.RemoveIDReturns(roomdb.ErrNotFound)
addVals = url.Values{"id": []string{"667"}}
rec = ts.Client.PostForm(urlRemove.String(), addVals)
a.Equal(http.StatusNotFound, rec.Code)
//TODO: update redirect code with flash errors
}

View File

@ -28,22 +28,26 @@ var HTMLTemplates = []string{
"admin/aliases.tmpl",
"admin/aliases-revoke-confirm.tmpl",
"admin/members.tmpl",
"admin/members-remove-confirm.tmpl",
"admin/denied-keys.tmpl",
"admin/denied-keys-remove-confirm.tmpl",
"admin/invite-list.tmpl",
"admin/invite-revoke-confirm.tmpl",
"admin/invite-created.tmpl",
"admin/notice-edit.tmpl",
"admin/members.tmpl",
"admin/members-remove-confirm.tmpl",
}
// Databases is an option struct that encapsualtes the required database services
type Databases struct {
Members roomdb.MembersService
Aliases roomdb.AliasesService
DeniedKeys roomdb.DeniedKeysService
Invites roomdb.InvitesService
Notices roomdb.NoticesService
Members roomdb.MembersService
PinnedNotices roomdb.PinnedNoticesService
}
@ -77,6 +81,15 @@ func Handler(
mux.HandleFunc("/aliases/revoke/confirm", r.HTML("admin/aliases-revoke-confirm.tmpl", ah.revokeConfirm))
mux.HandleFunc("/aliases/revoke", ah.revoke)
var dh = deniedKeysHandler{
r: r,
db: dbs.DeniedKeys,
}
mux.HandleFunc("/denied", r.HTML("admin/denied-keys.tmpl", dh.overview))
mux.HandleFunc("/denied/add", dh.add)
mux.HandleFunc("/denied/remove/confirm", r.HTML("admin/denied-keys-remove-confirm.tmpl", dh.removeConfirm))
mux.HandleFunc("/denied/remove", dh.remove)
var mh = membersHandler{
r: r,
db: dbs.Members,

View File

@ -90,9 +90,6 @@ func (h membersHandler) overview(rw http.ResponseWriter, req *http.Request) (int
return pageData, nil
}
// 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 membersHandler) removeConfirm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
id, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
if err != nil {

View File

@ -29,11 +29,12 @@ type testSession struct {
Client *tester.Tester
Router *mux.Router
AliasesDB *mockdb.FakeAliasesService
MembersDB *mockdb.FakeMembersService
PinnedDB *mockdb.FakePinnedNoticesService
NoticeDB *mockdb.FakeNoticesService
InvitesDB *mockdb.FakeInvitesService
AliasesDB *mockdb.FakeAliasesService
DeniedKeysDB *mockdb.FakeDeniedKeysService
InvitesDB *mockdb.FakeInvitesService
NoticeDB *mockdb.FakeNoticesService
MembersDB *mockdb.FakeMembersService
PinnedDB *mockdb.FakePinnedNoticesService
User *roomdb.Member
@ -47,6 +48,7 @@ func newSession(t *testing.T) *testSession {
// fake dbs
ts.AliasesDB = new(mockdb.FakeAliasesService)
ts.DeniedKeysDB = new(mockdb.FakeDeniedKeysService)
ts.MembersDB = new(mockdb.FakeMembersService)
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
ts.NoticeDB = new(mockdb.FakeNoticesService)
@ -102,6 +104,7 @@ func newSession(t *testing.T) *testSession {
ts.RoomState,
Databases{
Aliases: ts.AliasesDB,
DeniedKeys: ts.DeniedKeysDB,
Members: ts.MembersDB,
Invites: ts.InvitesDB,
Notices: ts.NoticeDB,

View File

@ -0,0 +1,31 @@
{{ define "title" }}{{i18n "AdminAllowListRemoveConfirmTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<span
id="welcome"
class="text-center"
>{{i18n "AdminAllowListRemoveConfirmWelcome"}}</span>
<pre
id="verify"
class="my-4 font-mono truncate max-w-full text-lg text-gray-700"
>{{.Entry.PubKey.Ref}}</pre>
<form id="confirm" action="{{urlTo "admin:allow-list:remove"}}" method="POST">
{{ .csrfField }}
<input type="hidden" name="id" value={{.Entry.ID}}>
<div class="grid grid-cols-2 gap-4">
<a
href="javascript:history.back()"
class="px-4 h-8 shadow rounded flex flex-row justify-center items-center bg-white align-middle text-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-300 focus:ring-opacity-50"
>{{i18n "GenericGoBack"}}</a>
<button
type="submit"
class="shadow rounded px-4 h-8 text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
>{{i18n "GenericConfirm"}}</button>
</div>
</form>
</div>
{{end}}

View File

@ -0,0 +1,89 @@
{{ define "title" }}{{i18n "AdminDeniedKeysTitle"}}{{ end }}
{{ define "content" }}
<h1
class="text-3xl tracking-tight font-black text-black mt-2 mb-4"
>{{i18n "AdminDeniedKeysTitle"}}</h1>
<p id="welcome" class="my-2">{{i18n "AdminDeniedKeysWelcome"}}</p>
<p
id="DeniedKeysCount"
class="text-lg font-bold my-2"
>{{i18npl "MemberCount" .Count}}</p>
<ul id="theList" class="divide-y pb-4">
<form
id="add-entry"
action="{{urlTo "admin:denied-keys:add"}}"
method="POST"
>
{{ .csrfField }}
<div class="flex flex-row items-center h-12">
<input
type="text"
name="pub_key"
placeholder="@ .ed25519"
class="font-mono truncate flex-auto tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
>
<input
type="submit"
value="{{i18n "AdminDeniedKeysAdd"}}"
class="pl-4 w-20 py-2 text-center text-green-500 hover:text-green-600 font-bold bg-transparent cursor-pointer"
>
</div>
</form>
{{range .Entries}}
<li class="flex flex-row items-center h-12">
<span
class="font-mono truncate flex-auto text-gray-600 tracking-wider"
>{{.PubKey.Ref}}</span>
<a
href="{{urlTo "admin:denied-keys:remove:confirm" "id" .ID}}"
class="pl-4 w-20 py-2 text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
>{{i18n "AdminDeniedKeysRemove"}}</a>
</li>
{{end}}
</ul>
{{$pageNums := .Paginator.PageNums}}
{{$view := .View}}
{{if gt $pageNums 1}}
<div class="flex flex-row justify-center">
{{if not .FirstInView}}
<a
href="{{urlTo "admin:denied-keys:overview"}}?page=1"
class="rounded px-3 py-2 text-pink-600 border-transparent hover:border-pink-400 border-2"
>1</a>
<span
class="px-3 py-2 text-gray-400 border-2 border-transparent"
>..</span>
{{end}}
{{range $view.Pages}}
{{if le . $pageNums}}
{{if eq . $view.Current}}
<span
class="px-3 py-2 cursor-default text-gray-500 border-2 border-transparent"
>{{.}}</span>
{{else}}
<a
href="{{urlTo "admin:denied-keys:overview"}}?page={{.}}"
class="rounded px-3 py-2 mx-1 text-pink-600 border-transparent hover:border-pink-400 border-2"
>{{.}}</a>
{{end}}
{{end}}
{{end}}
{{if not .LastInView}}
<span
class="px-3 py-2 text-gray-400 border-2 border-transparent"
>..</span>
<a
href="{{urlTo "admin:denied-keys:overview"}}?page={{$view.Last}}"
class="rounded px-3 py-2 text-pink-600 border-transparent hover:border-pink-400 border-2"
>{{$view.Last}}</a>
{{end}}
</div>
{{end}}
{{end}}