Invite endpoints

* Add revoke and created templates
* Render invite accept with domain
* Flesh out accept page
This commit is contained in:
Henry 2021-03-04 15:09:14 +01:00
parent 9f1fef1916
commit 98468e93a5
22 changed files with 415 additions and 46 deletions

View File

@ -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,

View File

@ -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),

View File

@ -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) {

View File

@ -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)
}

View File

@ -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,

View File

@ -1,13 +1,18 @@
package admin
import (
"errors"
"fmt"
"net/http"
"go.mindeco.de/http/render"
"strconv"
"github.com/gorilla/csrf"
"go.mindeco.de/http/render"
"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
}
http.Redirect(w, req, "/admin/invites", http.StatusFound)
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(rw, req, redirectToInvites, status)
}

View File

@ -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)
}

View File

@ -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)))

View File

@ -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")

60
web/handlers/invites.go Normal file
View File

@ -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
}

View File

@ -0,0 +1 @@
package handlers

View File

@ -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,
)

View File

@ -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."

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,23 @@
{{ define "title" }}{{i18n "AdminInviteCreatedTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<span
id="welcome"
class="text-center"
>{{i18n "AdminInviteCreatedWelcome"}}</span>
<a
href="{{urlTo "complete:invite:accept" "token" .Token}}"
id="accept-link"
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 "InviteAccept"}}</a>
<pre id="invite-accept-link">{{.AccepURL}}</pre>
{{if ne .AliasSuggestion ""}}
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
<p>{{i18n "AdminInviteSuggestedAliasIs"}} {{.AliasSuggestion}}</p>
{{end}}
</div>
{{end}}

View File

@ -36,12 +36,18 @@
<li class="flex flex-row items-center h-12">
<span
class="font-mono truncate flex-auto text-gray-600 tracking-wider"
>{{.CreatedBy.Name}}</span>
>
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
{{i18n "AdminInviteCreatedBy"}} {{.CreatedBy.Name}}
{{if ne .AliasSuggestion ""}}
({{i18n "AdminInviteSuggestedAliasIsShort"}} {{.AliasSuggestion}})
{{end}}
</span>
<a
href="{{urlTo "admin:invites:revoke:confirm" "id" .ID}}"
class="pl-4 w-20 py-2 text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
>{{i18n "AdminInvitesRevoke"}}</a>
>{{i18n "AdminInviteRevoke"}}</a>
</li>
{{end}}
</ul>

View File

@ -0,0 +1,35 @@
{{ define "title" }}{{i18n "AdminInviteRevokeConfirmTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<span
id="welcome"
class="text-center"
>{{i18n "AdminInviteRevokeConfirmWelcome"}}</span>
<pre
class="my-4 font-mono truncate max-w-full text-lg text-gray-700"
>{{.Invite.CreatedBy.Name}}</pre>
{{if ne .Invite.AliasSuggestion ""}}
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
<p>{{i18n "AdminInviteSuggestedAliasIs"}} {{.Invite.AliasSuggestion}}</p>
{{end}}
<form id="confirm" action="{{urlTo "admin:invites:revoke"}}" method="POST">
{{ .csrfField }}
<input type="hidden" name="id" value={{.Invite.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,16 @@
{{ define "title" }}{{i18n "InviteAcceptTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<span
id="welcome"
class="text-center"
>{{i18n "InviteAcceptWelcome"}}</span>
{{if ne .AliasSuggestion ""}}
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
<p>{{i18n "InviteAcceptAliasSuggestion"}} {{.AliasSuggestion}}</p>
{{end}}
</div>
{{end}}

View File

@ -0,0 +1,12 @@
{{ define "title" }}{{i18n "InviteConsumedTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<span
id="welcome"
class="text-center"
>{{i18n "InviteConsumedWelcome"}}</span>
<p class="color-red-600">TODO: present tunnel address and ssb uri redirect</p>
</div>
{{end}}

21
web/user/testing.go Normal file
View File

@ -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))
})
}
}

View File

@ -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{}
}
}