Add support for invite consumption via JSON
This commit is contained in:
parent
a67063ac32
commit
d1d108b9b7
|
@ -131,7 +131,7 @@ func runroomsrv() error {
|
||||||
if !development {
|
if !development {
|
||||||
return fmt.Errorf("https-domain can't be empty. See '%s -h' for a full list of options", os.Args[0])
|
return fmt.Errorf("https-domain can't be empty. See '%s -h' for a full list of options", os.Args[0])
|
||||||
}
|
}
|
||||||
httpsDomain = "dev.testing.local"
|
httpsDomain = "localhost"
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate listen addresses to bail out on invalid flag input before doing anything else
|
// validate listen addresses to bail out on invalid flag input before doing anything else
|
||||||
|
@ -254,6 +254,8 @@ func runroomsrv() error {
|
||||||
PortHTTPS: uint(portHTTP),
|
PortHTTPS: uint(portHTTP),
|
||||||
PortMUXRPC: uint(portMUXRPC),
|
PortMUXRPC: uint(portMUXRPC),
|
||||||
RoomID: roomsrv.Whoami(),
|
RoomID: roomsrv.Whoami(),
|
||||||
|
|
||||||
|
Development: development,
|
||||||
},
|
},
|
||||||
roomsrv.StateManager,
|
roomsrv.StateManager,
|
||||||
roomsrv.Network,
|
roomsrv.Network,
|
||||||
|
|
|
@ -27,6 +27,8 @@ type ServerEndpointDetails struct {
|
||||||
RoomID refs.FeedRef
|
RoomID refs.FeedRef
|
||||||
|
|
||||||
Domain string
|
Domain string
|
||||||
|
|
||||||
|
Development bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiserverAddress returns net:domain:muxport~shs:roomPubKeyInBase64
|
// MultiserverAddress returns net:domain:muxport~shs:roomPubKeyInBase64
|
||||||
|
|
|
@ -162,7 +162,7 @@ func (i Invites) GetByToken(ctx context.Context, token string) (roomdb.Invite, e
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := models.Invites(
|
entry, err := models.Invites(
|
||||||
qm.Where("active = true AND token = ?", ht),
|
qm.Where("active = true AND hashed_token = ?", ht),
|
||||||
qm.Load("CreatedByMember"),
|
qm.Load("CreatedByMember"),
|
||||||
).One(ctx, i.db)
|
).One(ctx, i.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -79,6 +79,10 @@ func TestInvites(t *testing.T) {
|
||||||
_, nope := db.Members.GetByFeed(ctx, newMember)
|
_, nope := db.Members.GetByFeed(ctx, newMember)
|
||||||
r.Error(nope, "expected feed to not yet be on the allow list")
|
r.Error(nope, "expected feed to not yet be on the allow list")
|
||||||
|
|
||||||
|
gotInv, err := db.Invites.GetByToken(ctx, tok)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal(lst[0].ID, gotInv.ID)
|
||||||
|
|
||||||
inv, err := db.Invites.Consume(ctx, tok, newMember)
|
inv, err := db.Invites.Consume(ctx, tok, newMember)
|
||||||
r.NoError(err, "failed to consume the invite")
|
r.NoError(err, "failed to consume the invite")
|
||||||
r.Equal(testMemberNick, inv.CreatedBy.Nickname)
|
r.Equal(testMemberNick, inv.CreatedBy.Nickname)
|
||||||
|
|
|
@ -67,13 +67,12 @@ func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interf
|
||||||
}
|
}
|
||||||
|
|
||||||
urlTo := web.NewURLTo(router.CompleteApp())
|
urlTo := web.NewURLTo(router.CompleteApp())
|
||||||
acceptURL := urlTo(router.CompleteInviteAccept, "token", token)
|
facadeURL := urlTo(router.CompleteInviteFacade, "token", token)
|
||||||
acceptURL.Host = h.domainName
|
facadeURL.Host = h.domainName
|
||||||
acceptURL.Scheme = "https"
|
facadeURL.Scheme = "https"
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"Token": token,
|
"FacadeURL": facadeURL.String(),
|
||||||
"AccepURL": acceptURL.String(),
|
|
||||||
|
|
||||||
"AliasSuggestion": aliasSuggestion,
|
"AliasSuggestion": aliasSuggestion,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
@ -128,10 +128,10 @@ func TestInvitesCreate(t *testing.T) {
|
||||||
{"#welcome", "AdminInviteCreatedWelcome"},
|
{"#welcome", "AdminInviteCreatedWelcome"},
|
||||||
})
|
})
|
||||||
|
|
||||||
wantURL := urlTo(router.CompleteInviteAccept, "token", testInvite)
|
wantURL := urlTo(router.CompleteInviteFacade, "token", testInvite)
|
||||||
wantURL.Host = ts.Domain
|
wantURL.Host = ts.Domain
|
||||||
wantURL.Scheme = "https"
|
wantURL.Scheme = "https"
|
||||||
|
|
||||||
shownLink := doc.Find("#invite-accept-link").Text()
|
shownLink := doc.Find("#invite-facade-link").Text()
|
||||||
a.Equal(wantURL.String(), shownLink)
|
a.Equal(wantURL.String(), shownLink)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
|
|
||||||
var HTMLTemplates = []string{
|
var HTMLTemplates = []string{
|
||||||
"auth/decide_method.tmpl",
|
"auth/decide_method.tmpl",
|
||||||
|
"auth/fallback_sign_in.tmpl",
|
||||||
"auth/withssb_server_start.tmpl",
|
"auth/withssb_server_start.tmpl",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -431,8 +431,7 @@ func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
|
||||||
a.Equal("start-http-auth", qry.Get("action"))
|
a.Equal("start-http-auth", qry.Get("action"))
|
||||||
a.Equal(serverChallenge, qry.Get("sc"))
|
a.Equal(serverChallenge, qry.Get("sc"))
|
||||||
a.Equal(ts.NetworkInfo.RoomID.Ref(), qry.Get("sid"))
|
a.Equal(ts.NetworkInfo.RoomID.Ref(), qry.Get("sid"))
|
||||||
var msaddr = fmt.Sprintf("net:%s:%d~shs:%s", ts.NetworkInfo.Domain, ts.NetworkInfo.PortMUXRPC, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey()))
|
a.Equal(ts.NetworkInfo.MultiserverAddress(), qry.Get("multiserverAddress"))
|
||||||
a.Equal(msaddr, qry.Get("multiserverAddress"))
|
|
||||||
|
|
||||||
qrCode, has := html.Find("#start-auth-qrcode").Attr("src")
|
qrCode, has := html.Find("#start-auth-qrcode").Attr("src")
|
||||||
a.True(has, "should have the inline image data")
|
a.True(has, "should have the inline image data")
|
||||||
|
|
|
@ -36,11 +36,13 @@ var HTMLTemplates = []string{
|
||||||
"landing/index.tmpl",
|
"landing/index.tmpl",
|
||||||
"landing/about.tmpl",
|
"landing/about.tmpl",
|
||||||
"aliases-resolved.html",
|
"aliases-resolved.html",
|
||||||
"invite/accept.tmpl",
|
|
||||||
"invite/consumed.tmpl",
|
"invite/consumed.tmpl",
|
||||||
"auth/fallback_sign_in.tmpl",
|
"invite/facade.tmpl",
|
||||||
|
|
||||||
"notice/list.tmpl",
|
"notice/list.tmpl",
|
||||||
"notice/show.tmpl",
|
"notice/show.tmpl",
|
||||||
|
|
||||||
"error.tmpl",
|
"error.tmpl",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +96,7 @@ func New(
|
||||||
}),
|
}),
|
||||||
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
|
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
|
||||||
return func(routeName string) bool {
|
return func(routeName string) bool {
|
||||||
route := router.CompleteApp().Get(routeName)
|
route := m.Get(routeName)
|
||||||
if route == nil {
|
if route == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -115,7 +117,7 @@ func New(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
route := router.CompleteApp().GetRoute(router.CompleteNoticeShow)
|
route := m.GetRoute(router.CompleteNoticeShow)
|
||||||
if route == nil {
|
if route == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -220,8 +222,6 @@ func New(
|
||||||
mainMux := &http.ServeMux{}
|
mainMux := &http.ServeMux{}
|
||||||
|
|
||||||
// start hooking up handlers to the router
|
// start hooking up handlers to the router
|
||||||
var muxrpcHostAndPort = fmt.Sprintf("%s:%d", netInfo.Domain, netInfo.PortMUXRPC)
|
|
||||||
|
|
||||||
authWithSSB := roomsAuth.NewWithSSBHandler(
|
authWithSSB := roomsAuth.NewWithSSBHandler(
|
||||||
m,
|
m,
|
||||||
r,
|
r,
|
||||||
|
@ -299,13 +299,14 @@ func New(
|
||||||
m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve)
|
m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve)
|
||||||
|
|
||||||
var ih = inviteHandler{
|
var ih = inviteHandler{
|
||||||
|
render: r,
|
||||||
|
|
||||||
invites: dbs.Invites,
|
invites: dbs.Invites,
|
||||||
|
|
||||||
roomPubKey: netInfo.RoomID.PubKey(),
|
networkInfo: netInfo,
|
||||||
muxrpcHostAndPort: muxrpcHostAndPort,
|
|
||||||
}
|
}
|
||||||
m.Get(router.CompleteInviteAccept).Handler(r.HTML("invite/accept.tmpl", ih.acceptForm))
|
m.Get(router.CompleteInviteFacade).Handler(r.HTML("invite/facade.tmpl", ih.presentFacade))
|
||||||
m.Get(router.CompleteInviteConsume).Handler(r.HTML("invite/consumed.tmpl", ih.consume))
|
m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume)
|
||||||
|
|
||||||
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
|
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
|
||||||
|
|
||||||
|
@ -317,11 +318,27 @@ func New(
|
||||||
|
|
||||||
mainMux.Handle("/", m)
|
mainMux.Handle("/", m)
|
||||||
|
|
||||||
|
urlTo := web.NewURLTo(m)
|
||||||
|
consumeURL := urlTo(router.CompleteInviteConsume)
|
||||||
|
|
||||||
// apply HTTP middleware
|
// apply HTTP middleware
|
||||||
middlewares := []func(http.Handler) http.Handler{
|
middlewares := []func(http.Handler) http.Handler{
|
||||||
logging.InjectHandler(logger),
|
logging.InjectHandler(logger),
|
||||||
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
|
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
|
||||||
CSRF,
|
CSRF,
|
||||||
|
|
||||||
|
// We disable CSRF for certain requests that are done by apps
|
||||||
|
// only if they already contain some secret (like invite consumption)
|
||||||
|
func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
ct := req.Header.Get("Content-Type")
|
||||||
|
if req.URL.Path == consumeURL.Path && ct == "application/json" {
|
||||||
|
next.ServeHTTP(w, csrf.UnsafeSkipCheck(req))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if !web.Production {
|
if !web.Production {
|
||||||
|
|
|
@ -1,30 +1,35 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"go.mindeco.de/logging"
|
|
||||||
"golang.org/x/crypto/ed25519"
|
|
||||||
|
|
||||||
"github.com/go-kit/kit/log/level"
|
"github.com/go-kit/kit/log/level"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
|
"go.mindeco.de/http/render"
|
||||||
|
"go.mindeco.de/logging"
|
||||||
|
|
||||||
|
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||||
|
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||||
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||||
refs "go.mindeco.de/ssb-refs"
|
refs "go.mindeco.de/ssb-refs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type inviteHandler struct {
|
type inviteHandler struct {
|
||||||
invites roomdb.InvitesService
|
render *render.Renderer
|
||||||
aliases roomdb.AliasesService
|
|
||||||
|
|
||||||
muxrpcHostAndPort string
|
invites roomdb.InvitesService
|
||||||
roomPubKey ed25519.PublicKey
|
|
||||||
|
networkInfo network.ServerEndpointDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
token := req.URL.Query().Get("token")
|
token := req.URL.Query().Get("token")
|
||||||
|
|
||||||
inv, err := h.invites.GetByToken(req.Context(), token)
|
inv, err := h.invites.GetByToken(req.Context(), token)
|
||||||
|
@ -35,49 +40,201 @@ func (h inviteHandler) acceptForm(rw http.ResponseWriter, req *http.Request) (in
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
var joinRoomURI url.URL
|
||||||
"Token": token,
|
joinRoomURI.Scheme = "ssb"
|
||||||
"Invite": inv,
|
joinRoomURI.Opaque = "experimental"
|
||||||
|
|
||||||
|
queryVals := make(url.Values)
|
||||||
|
queryVals.Set("action", "join-room")
|
||||||
|
queryVals.Set("invite", token)
|
||||||
|
|
||||||
|
urlTo := web.NewURLTo(router.CompleteApp())
|
||||||
|
submissionURL := urlTo(router.CompleteInviteConsume)
|
||||||
|
submissionURL.Host = h.networkInfo.Domain
|
||||||
|
submissionURL.Scheme = "https"
|
||||||
|
if h.networkInfo.Development {
|
||||||
|
submissionURL.Scheme = "http"
|
||||||
|
submissionURL.Host += fmt.Sprintf(":%d", h.networkInfo.PortHTTPS)
|
||||||
|
}
|
||||||
|
queryVals.Set("postTo", submissionURL.String())
|
||||||
|
|
||||||
|
joinRoomURI.RawQuery = queryVals.Encode()
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
csrf.TemplateTag: csrf.TemplateField(req),
|
csrf.TemplateTag: csrf.TemplateField(req),
|
||||||
|
|
||||||
|
"Invite": inv,
|
||||||
|
"Token": token,
|
||||||
|
|
||||||
|
"JoinRoomURI": template.URL(joinRoomURI.String()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
type inviteConsumePayload struct {
|
||||||
if err := req.ParseForm(); err != nil {
|
ID refs.FeedRef `json:"id"`
|
||||||
return nil, weberrors.ErrBadRequest{Where: "form data", Details: err}
|
Invite string `json:"invite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
logger := logging.FromContext(req.Context())
|
||||||
|
|
||||||
|
var (
|
||||||
|
token string
|
||||||
|
newMember refs.FeedRef
|
||||||
|
|
||||||
|
resp inviteConsumeResponder
|
||||||
|
)
|
||||||
|
ct := req.Header.Get("Content-Type")
|
||||||
|
switch ct {
|
||||||
|
case "application/json":
|
||||||
|
resp = newinviteConsumeJSONResponder(rw)
|
||||||
|
|
||||||
|
var body inviteConsumePayload
|
||||||
|
|
||||||
|
level.Debug(logger).Log("event", "handling json body")
|
||||||
|
err := json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("consume body contained invalid json: %w", err)
|
||||||
|
resp.SendError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newMember = body.ID
|
||||||
|
token = body.Invite
|
||||||
|
|
||||||
|
case "application/x-www-form-urlencoded":
|
||||||
|
resp = newinviteConsumeHTMLResponder(h.render, rw, req)
|
||||||
|
|
||||||
|
if err := req.ParseForm(); err != nil {
|
||||||
|
err = weberrors.ErrBadRequest{Where: "form data", Details: err}
|
||||||
|
resp.SendError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token = req.FormValue("invite")
|
||||||
|
|
||||||
|
parsedID, err := refs.ParseFeedRef(req.FormValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
err = weberrors.ErrBadRequest{Where: "id", Details: err}
|
||||||
|
resp.SendError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newMember = *parsedID
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(rw, fmt.Sprintf("unhandled Content-Type (%q)", ct), http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
resp.UpdateMultiserverAddr(h.networkInfo.MultiserverAddress())
|
||||||
|
|
||||||
alias := req.FormValue("alias")
|
inv, err := h.invites.Consume(req.Context(), token, newMember)
|
||||||
token := req.FormValue("token")
|
|
||||||
|
|
||||||
newMember, err := refs.ParseFeedRef(req.FormValue("new_member"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, weberrors.ErrBadRequest{Where: "new_member", Details: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
inv, err := h.invites.Consume(req.Context(), token, *newMember)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, roomdb.ErrNotFound) {
|
if errors.Is(err, roomdb.ErrNotFound) {
|
||||||
return nil, weberrors.ErrNotFound{What: "invite"}
|
resp.SendError(weberrors.ErrNotFound{What: "invite"})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return nil, err
|
resp.SendError(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
log := logging.FromContext(req.Context())
|
log := logging.FromContext(req.Context())
|
||||||
level.Info(log).Log("event", "invite consumed", "id", inv.ID, "ref", newMember.ShortRef())
|
level.Info(log).Log("event", "invite consumed", "id", inv.ID, "ref", newMember.ShortRef())
|
||||||
|
|
||||||
if alias != "" {
|
resp.SendSuccess()
|
||||||
level.Warn(log).Log(
|
}
|
||||||
"TODO", "invite registration",
|
|
||||||
"alias", alias,
|
// inviteConsumeResponder is supposed to handle different encoding types transparently.
|
||||||
)
|
// It either sends the rooms multiaddress on success or an error.
|
||||||
|
type inviteConsumeResponder interface {
|
||||||
|
SendSuccess()
|
||||||
|
SendError(error)
|
||||||
|
|
||||||
|
UpdateMultiserverAddr(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inviteConsumeJSONResponse dictates the field names and format of the JSON response for the inviteConsume web endpoint
|
||||||
|
type inviteConsumeJSONResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
|
||||||
|
RoomAddress string `json:"multiserverAddress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles JSON responses
|
||||||
|
type inviteConsumeJSONResponder struct {
|
||||||
|
enc *json.Encoder
|
||||||
|
|
||||||
|
multiservAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newinviteConsumeJSONResponder(rw http.ResponseWriter) inviteConsumeResponder {
|
||||||
|
rw.Header().Set("Content-Type", "application/json")
|
||||||
|
return &inviteConsumeJSONResponder{
|
||||||
|
enc: json.NewEncoder(rw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (json *inviteConsumeJSONResponder) UpdateMultiserverAddr(msaddr string) {
|
||||||
|
json.multiservAddr = msaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (json inviteConsumeJSONResponder) SendSuccess() {
|
||||||
|
var resp = inviteConsumeJSONResponse{
|
||||||
|
Status: "successful",
|
||||||
|
RoomAddress: json.multiservAddr,
|
||||||
|
}
|
||||||
|
json.enc.Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (json inviteConsumeJSONResponder) SendError(err error) {
|
||||||
|
json.enc.Encode(struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{"error", err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles HTML responses
|
||||||
|
type inviteConsumeHTMLResponder struct {
|
||||||
|
renderer *render.Renderer
|
||||||
|
rw http.ResponseWriter
|
||||||
|
req *http.Request
|
||||||
|
|
||||||
|
multiservAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newinviteConsumeHTMLResponder(r *render.Renderer, rw http.ResponseWriter, req *http.Request) inviteConsumeResponder {
|
||||||
|
return &inviteConsumeHTMLResponder{
|
||||||
|
renderer: r,
|
||||||
|
rw: rw,
|
||||||
|
req: req,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (html *inviteConsumeHTMLResponder) UpdateMultiserverAddr(msaddr string) {
|
||||||
|
html.multiservAddr = msaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (html inviteConsumeHTMLResponder) SendSuccess() {
|
||||||
|
|
||||||
|
// construct the ssb:experimental?action=consume-invite&... uri for linking into apps
|
||||||
|
queryParams := url.Values{}
|
||||||
|
queryParams.Set("action", "join-room")
|
||||||
|
queryParams.Set("multiserverAddress", html.multiservAddr)
|
||||||
|
|
||||||
|
// html.multiservAddr
|
||||||
|
ssbURI := url.URL{
|
||||||
|
Scheme: "ssb",
|
||||||
|
Opaque: "experimental",
|
||||||
|
RawQuery: queryParams.Encode(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: hardcoded here just to be replaced soon with next version of ssb-uri
|
err := html.renderer.Render(html.rw, html.req, "invite/consumed.tmpl", http.StatusOK, struct {
|
||||||
roomPubKey := base64.StdEncoding.EncodeToString(h.roomPubKey)
|
SSBURI template.URL
|
||||||
roomAddr := fmt.Sprintf("net:%s~shs:%s:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", h.muxrpcHostAndPort, roomPubKey)
|
}{template.URL(ssbURI.String())})
|
||||||
|
if err != nil {
|
||||||
return map[string]interface{}{
|
logger := logging.FromContext(html.req.Context())
|
||||||
"RoomAddress": roomAddr,
|
level.Warn(logger).Log("event", "render failed", "err", err)
|
||||||
}, nil
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (html inviteConsumeHTMLResponder) SendError(err error) {
|
||||||
|
html.renderer.Error(html.rw, html.req, http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -10,16 +11,15 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
refs "go.mindeco.de/ssb-refs"
|
|
||||||
|
|
||||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
"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"
|
||||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
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/router"
|
||||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
|
||||||
|
refs "go.mindeco.de/ssb-refs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInviteShowAcceptForm(t *testing.T) {
|
func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
|
@ -31,7 +31,7 @@ func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
a, r := assert.New(t), require.New(t)
|
a, r := assert.New(t), require.New(t)
|
||||||
|
|
||||||
testToken := "nonexistant-test-token"
|
testToken := "nonexistant-test-token"
|
||||||
acceptURL404 := urlTo(router.CompleteInviteAccept, "token", testToken)
|
acceptURL404 := urlTo(router.CompleteInviteFacade, "token", testToken)
|
||||||
r.NotNil(acceptURL404)
|
r.NotNil(acceptURL404)
|
||||||
|
|
||||||
// prep the mocked db for http:404
|
// prep the mocked db for http:404
|
||||||
|
@ -65,7 +65,7 @@ func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
a, r := assert.New(t), require.New(t)
|
a, r := assert.New(t), require.New(t)
|
||||||
|
|
||||||
testToken := "existing-test-token"
|
testToken := "existing-test-token"
|
||||||
validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken)
|
validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken)
|
||||||
r.NotNil(validAcceptURL)
|
r.NotNil(validAcceptURL)
|
||||||
|
|
||||||
// prep the mocked db for http:200
|
// prep the mocked db for http:200
|
||||||
|
@ -87,8 +87,8 @@ func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
a.Equal(testToken, tokenFromArg)
|
a.Equal(testToken, tokenFromArg)
|
||||||
|
|
||||||
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
||||||
{"#welcome", "InviteAcceptWelcome"},
|
{"#welcome", "InviteFacadeWelcome"},
|
||||||
{"title", "InviteAcceptTitle"},
|
{"title", "InviteFacadeTitle"},
|
||||||
})
|
})
|
||||||
|
|
||||||
form := doc.Find("form#consume")
|
form := doc.Find("form#consume")
|
||||||
|
@ -97,9 +97,8 @@ func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
webassert.CSRFTokenPresent(t, form)
|
webassert.CSRFTokenPresent(t, form)
|
||||||
|
|
||||||
webassert.ElementsInForm(t, form, []webassert.FormElement{
|
webassert.ElementsInForm(t, form, []webassert.FormElement{
|
||||||
{Name: "token", Type: "hidden", Value: testToken},
|
{Name: "invite", Type: "hidden", Value: testToken},
|
||||||
{Name: "alias", Type: "text", Value: fakeExistingInvite.AliasSuggestion},
|
{Name: "id", Type: "text", Placeholder: wantNewMemberPlaceholder},
|
||||||
{Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -107,11 +106,11 @@ func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
a, r := assert.New(t), require.New(t)
|
a, r := assert.New(t), require.New(t)
|
||||||
|
|
||||||
testToken := "existing-test-token-2"
|
testToken := "existing-test-token-2"
|
||||||
validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken)
|
validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken)
|
||||||
r.NotNil(validAcceptURL)
|
r.NotNil(validAcceptURL)
|
||||||
|
|
||||||
inviteWithNoAlias := roomdb.Invite{ID: 4321}
|
testInvite := roomdb.Invite{ID: 4321}
|
||||||
ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil)
|
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
|
||||||
|
|
||||||
// request the form
|
// request the form
|
||||||
validAcceptForm := validAcceptURL.String()
|
validAcceptForm := validAcceptURL.String()
|
||||||
|
@ -125,8 +124,8 @@ func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
a.Equal(testToken, tokenFromArg)
|
a.Equal(testToken, tokenFromArg)
|
||||||
|
|
||||||
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
||||||
{"#welcome", "InviteAcceptWelcome"},
|
{"#welcome", "InviteFacadeWelcome"},
|
||||||
{"title", "InviteAcceptTitle"},
|
{"title", "InviteFacadeTitle"},
|
||||||
})
|
})
|
||||||
|
|
||||||
form := doc.Find("form#consume")
|
form := doc.Find("form#consume")
|
||||||
|
@ -134,26 +133,25 @@ func TestInviteShowAcceptForm(t *testing.T) {
|
||||||
|
|
||||||
webassert.CSRFTokenPresent(t, form)
|
webassert.CSRFTokenPresent(t, form)
|
||||||
webassert.ElementsInForm(t, form, []webassert.FormElement{
|
webassert.ElementsInForm(t, form, []webassert.FormElement{
|
||||||
{Name: "token", Type: "hidden", Value: testToken},
|
{Name: "invite", Type: "hidden", Value: testToken},
|
||||||
{Name: "alias", Type: "text", Placeholder: "you@this.room"},
|
{Name: "id", Type: "text", Placeholder: wantNewMemberPlaceholder},
|
||||||
{Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInviteConsumeInvite(t *testing.T) {
|
func TestInviteConsumeInviteHTTP(t *testing.T) {
|
||||||
ts := setup(t)
|
ts := setup(t)
|
||||||
a, r := assert.New(t), require.New(t)
|
a, r := assert.New(t), require.New(t)
|
||||||
urlTo := web.NewURLTo(ts.Router)
|
urlTo := web.NewURLTo(ts.Router)
|
||||||
|
|
||||||
testToken := "existing-test-token-2"
|
testToken := "existing-test-token-2"
|
||||||
validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken)
|
validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken)
|
||||||
r.NotNil(validAcceptURL)
|
r.NotNil(validAcceptURL)
|
||||||
validAcceptURL.Host = "localhost"
|
validAcceptURL.Host = "localhost"
|
||||||
validAcceptURL.Scheme = "https"
|
validAcceptURL.Scheme = "https"
|
||||||
|
|
||||||
inviteWithNoAlias := roomdb.Invite{ID: 4321}
|
testInvite := roomdb.Invite{ID: 4321}
|
||||||
ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil)
|
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
|
||||||
|
|
||||||
// request the form (for a valid csrf token)
|
// request the form (for a valid csrf token)
|
||||||
validAcceptForm := validAcceptURL.String()
|
validAcceptForm := validAcceptURL.String()
|
||||||
|
@ -184,8 +182,8 @@ func TestInviteConsumeInvite(t *testing.T) {
|
||||||
Algo: refs.RefAlgoFeedSSB1,
|
Algo: refs.RefAlgoFeedSSB1,
|
||||||
}
|
}
|
||||||
consumeVals := url.Values{
|
consumeVals := url.Values{
|
||||||
"token": []string{testToken},
|
"invite": []string{testToken},
|
||||||
"new_member": []string{testNewMember.Ref()},
|
"id": []string{testNewMember.Ref()},
|
||||||
|
|
||||||
csrfName: []string{csrfValue},
|
csrfName: []string{csrfValue},
|
||||||
}
|
}
|
||||||
|
@ -207,7 +205,7 @@ func TestInviteConsumeInvite(t *testing.T) {
|
||||||
ts.Client.SetHeaders(csrfCookieHeader)
|
ts.Client.SetHeaders(csrfCookieHeader)
|
||||||
|
|
||||||
// prepare the mock
|
// prepare the mock
|
||||||
ts.InvitesDB.ConsumeReturns(inviteWithNoAlias, nil)
|
ts.InvitesDB.ConsumeReturns(testInvite, nil)
|
||||||
|
|
||||||
// send the POST
|
// send the POST
|
||||||
resp = ts.Client.PostForm(consumeInviteURL.String(), consumeVals)
|
resp = ts.Client.PostForm(consumeInviteURL.String(), consumeVals)
|
||||||
|
@ -222,11 +220,75 @@ func TestInviteConsumeInvite(t *testing.T) {
|
||||||
consumedDoc, err := goquery.NewDocumentFromReader(resp.Body)
|
consumedDoc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
gotRA := consumedDoc.Find("#room-address").Text()
|
joinHref, ok := consumedDoc.Find("#join-link").Attr("href")
|
||||||
|
a.True(ok)
|
||||||
|
|
||||||
|
// validate ssb-uri
|
||||||
|
joinURI, err := url.Parse(joinHref)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
a.Equal("ssb", joinURI.Scheme)
|
||||||
|
a.Equal("experimental", joinURI.Opaque)
|
||||||
|
|
||||||
|
params := joinURI.Query()
|
||||||
|
a.Equal("join-room", params.Get("action"))
|
||||||
|
|
||||||
|
gotRA := params.Get("multiserverAddress")
|
||||||
|
|
||||||
// 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.HasPrefix(gotRA, "net:localhost:8008~shs:"), "not for the test host: %s", gotRA)
|
||||||
a.True(strings.Contains(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key missing? %s", gotRA)
|
a.True(strings.HasSuffix(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key missing? %s", gotRA)
|
||||||
a.True(strings.HasSuffix(gotRA, ":SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24="), "magic suffix missing: %s", gotRA)
|
}
|
||||||
|
|
||||||
|
func TestInviteConsumeInviteJSON(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.CompleteInviteFacade, "token", testToken)
|
||||||
|
r.NotNil(validAcceptURL)
|
||||||
|
validAcceptURL.Host = "localhost"
|
||||||
|
validAcceptURL.Scheme = "https"
|
||||||
|
|
||||||
|
testInvite := roomdb.Invite{ID: 4321}
|
||||||
|
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
|
||||||
|
|
||||||
|
// create the consume request
|
||||||
|
testNewMember := refs.FeedRef{
|
||||||
|
ID: bytes.Repeat([]byte{1}, 32),
|
||||||
|
Algo: refs.RefAlgoFeedSSB1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var consume inviteConsumePayload
|
||||||
|
consume.Invite = testToken
|
||||||
|
consume.ID = testNewMember
|
||||||
|
|
||||||
|
// construct the consume endpoint url
|
||||||
|
consumeInviteURL, err := ts.Router.Get(router.CompleteInviteConsume).URL()
|
||||||
|
r.Nil(err)
|
||||||
|
consumeInviteURL.Host = "localhost"
|
||||||
|
consumeInviteURL.Scheme = "https"
|
||||||
|
|
||||||
|
// prepare the mock
|
||||||
|
ts.InvitesDB.ConsumeReturns(testInvite, nil)
|
||||||
|
|
||||||
|
// send the POST
|
||||||
|
resp := ts.Client.SendJSON(consumeInviteURL.String(), consume)
|
||||||
|
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))
|
||||||
|
|
||||||
|
var jsonConsumeResp inviteConsumeJSONResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&jsonConsumeResp)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
a.Equal("successful", jsonConsumeResp.Status)
|
||||||
|
|
||||||
|
gotRA := jsonConsumeResp.RoomAddress
|
||||||
|
a.True(strings.HasPrefix(gotRA, "net:localhost:8008~shs:"), "not for the test host: %s", gotRA)
|
||||||
|
a.True(strings.HasSuffix(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key missing? %s", gotRA)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,11 +81,11 @@ NavAdminDashboard = "Dashboard"
|
||||||
NavAdminInvites = "Invites"
|
NavAdminInvites = "Invites"
|
||||||
NavAdminNotices = "Notices"
|
NavAdminNotices = "Notices"
|
||||||
|
|
||||||
InviteAccept = "Accept invite"
|
InviteFacade = "Join Room"
|
||||||
InviteAcceptTitle = "Accept Invite"
|
InviteFacadeTitle = "Join Room"
|
||||||
InviteAcceptWelcome = "elaborate welcome message for a new member with good words and stuff."
|
InviteFacadeWelcome = "elaborate welcome message for a new member with good words and stuff."
|
||||||
InviteAcceptAliasSuggestion = "The persone who created thought you might like this alias:"
|
InviteFacadeAliasSuggestion = "The persone who created thought you might like this alias:"
|
||||||
InviteAcceptPublicKey = "Public Key"
|
InviteFacadePublicKey = "Public Key"
|
||||||
|
|
||||||
InviteConsumedTitle = "Invite accepted!"
|
InviteConsumedTitle = "Invite accepted!"
|
||||||
InviteConsumedWelcome = "Even more elaborate message that the person is now a member of the room!"
|
InviteConsumedWelcome = "Even more elaborate message that the person is now a member of the room!"
|
||||||
|
|
|
@ -16,7 +16,7 @@ const (
|
||||||
|
|
||||||
CompleteAliasResolve = "complete:alias:resolve"
|
CompleteAliasResolve = "complete:alias:resolve"
|
||||||
|
|
||||||
CompleteInviteAccept = "complete:invite:accept"
|
CompleteInviteFacade = "complete:invite:accept"
|
||||||
CompleteInviteConsume = "complete:invite:consume"
|
CompleteInviteConsume = "complete:invite:consume"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ func CompleteApp() *mux.Router {
|
||||||
|
|
||||||
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
||||||
|
|
||||||
m.Path("/invite/accept").Methods("GET").Name(CompleteInviteAccept)
|
m.Path("/join").Methods("GET").Name(CompleteInviteFacade)
|
||||||
m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)
|
m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)
|
||||||
|
|
||||||
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)
|
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)
|
||||||
|
|
|
@ -7,13 +7,7 @@
|
||||||
class="text-center"
|
class="text-center"
|
||||||
>{{i18n "AdminInviteCreatedWelcome"}}</span>
|
>{{i18n "AdminInviteCreatedWelcome"}}</span>
|
||||||
|
|
||||||
<a
|
<pre id="invite-facade-link">{{.FacadeURL}}</pre>
|
||||||
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 ""}}
|
{{if ne .AliasSuggestion ""}}
|
||||||
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
|
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<pre
|
<pre
|
||||||
class="my-4 font-mono truncate max-w-full text-lg text-gray-700"
|
class="my-4 font-mono truncate max-w-full text-lg text-gray-700"
|
||||||
>{{.Invite.CreatedBy.Name}}</pre>
|
>{{.Invite.CreatedBy.Nickname}}</pre>
|
||||||
|
|
||||||
{{if ne .Invite.AliasSuggestion ""}}
|
{{if ne .Invite.AliasSuggestion ""}}
|
||||||
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
|
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
{{ 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>
|
|
||||||
|
|
||||||
<form id="consume" action="{{urlTo "complete:invite:consume"}}" method="POST">
|
|
||||||
{{ .csrfField }}
|
|
||||||
<input type="hidden" name="token" value={{.Token}}>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
|
|
||||||
<div class="my-4 flex flex-row items-center justify-start">
|
|
||||||
<label class="mr-2">{{ i18n "InviteAcceptPublicKey" }}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="new_member"
|
|
||||||
placeholder="@ .ed25519"
|
|
||||||
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent">
|
|
||||||
<span class="ml-2 text-red-400">TODO: make this a dropdown</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>{{ i18n "InviteAcceptAliasSuggestion" }}</p>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="alias"
|
|
||||||
{{ if ne .Invite.AliasSuggestion "" }}
|
|
||||||
value="{{ .Invite.AliasSuggestion }}"
|
|
||||||
{{else}}
|
|
||||||
placeholder="you@this.room"
|
|
||||||
{{ end }}
|
|
||||||
>
|
|
||||||
|
|
||||||
<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 }}
|
|
|
@ -7,7 +7,6 @@
|
||||||
class="text-center"
|
class="text-center"
|
||||||
>{{i18n "InviteConsumedWelcome"}}</span>
|
>{{i18n "InviteConsumedWelcome"}}</span>
|
||||||
|
|
||||||
<p class="color-red-600">TODO: this is just a <em>room v1 invite</em>. present tunnel address and ssb uri redirect</p>
|
<a id="join-link" href="{{.SSBURI}}">Join Room</a>
|
||||||
<pre id="room-address">{{.RoomAddress}}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
|
@ -0,0 +1,39 @@
|
||||||
|
{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="flex flex-col justify-center items-center h-64">
|
||||||
|
|
||||||
|
<span
|
||||||
|
id="welcome"
|
||||||
|
class="text-center"
|
||||||
|
>{{ i18n "InviteFacadeWelcome" }}</span>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{.JoinRoomURI}}"
|
||||||
|
class="my-8 shadow rounded px-4 h-8 text-gray-100 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-opacity-50"
|
||||||
|
>Join</a>
|
||||||
|
|
||||||
|
<hr class="mb-10 pt-10">
|
||||||
|
<h3 class="text-red-500">TODO: html form fallback / advanced use</h3>
|
||||||
|
|
||||||
|
<form id="consume" action="{{urlTo "complete:invite:consume"}}" method="POST">
|
||||||
|
{{ .csrfField }}
|
||||||
|
<input type="hidden" name="invite" value={{.Token}}>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="my-4 flex flex-row items-center justify-start">
|
||||||
|
<label class="mr-2">{{ i18n "InviteFacadePublicKey" }}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="id"
|
||||||
|
placeholder="@ .ed25519"
|
||||||
|
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue