add denied templates and handlers
copy of previous allow-list stuff
This commit is contained in:
parent
36d46a8576
commit
1b6f7f5006
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
|
@ -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}}
|
|
@ -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}}
|
Loading…
Reference in New Issue