Merge pull request #105 from ssb-ngi-pointer/update-invites

Add support for invite consumption via JSON
This commit is contained in:
Henry 2021-03-30 10:44:11 +02:00 committed by GitHub
commit 6409c79224
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 498 additions and 361 deletions

View File

@ -30,13 +30,13 @@ func main() {
check(err)
var (
name string
login string
pubKey *refs.FeedRef
role roomdb.Role = roomdb.RoleAdmin
repoPath string
)
flag.StringVar(&name, "name", "", "username (used when logging into the room's web ui)")
flag.StringVar(&login, "login", "", "username (used when logging into the room's web ui)")
flag.Func("key", "the public key of the user, format: @<base64-encoded public-key>.ed25519", func(val string) error {
if len(val) == 0 {
return fmt.Errorf("the public key is required. if you are just testing things out, generate one by running 'cmd/insert-user/generate-fake-id.sh'\n")
@ -73,8 +73,8 @@ func main() {
cliMissingArguments("please provide the default arguments -name and -key")
}
if name == "" {
cliMissingArguments("please provide a username with -name <username>")
if login == "" {
cliMissingArguments("please provide a username with -login <username>")
}
if pubKey == nil {
@ -101,13 +101,13 @@ func main() {
}
ctx := context.Background()
mid, err := db.Members.Add(ctx, name, *pubKey, role)
mid, err := db.Members.Add(ctx, *pubKey, role)
check(err)
err = db.AuthFallback.Create(ctx, mid, name, bytePassword)
err = db.AuthFallback.Create(ctx, mid, login, bytePassword)
check(err)
fmt.Fprintf(os.Stderr, "Created member %s (%s) with ID %d\n", name, role, mid)
fmt.Fprintf(os.Stderr, "Created member %s (%s) with ID %d\n", login, role, mid)
}
func cliMissingArguments(message string) {

View File

@ -131,7 +131,7 @@ func runroomsrv() error {
if !development {
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
@ -254,6 +254,8 @@ func runroomsrv() error {
PortHTTPS: uint(portHTTP),
PortMUXRPC: uint(portMUXRPC),
RoomID: roomsrv.Whoami(),
Development: development,
},
roomsrv.StateManager,
roomsrv.Network,

View File

@ -27,6 +27,8 @@ type ServerEndpointDetails struct {
RoomID refs.FeedRef
Domain string
Development bool
}
// MultiserverAddress returns net:domain:muxport~shs:roomPubKeyInBase64

View File

@ -68,12 +68,12 @@ func TestAliasRegister(t *testing.T) {
})
// adds
_, err = srvMembers.Add(ctx, "bob", bob.Whoami(), roomdb.RoleMember)
_, err = srvMembers.Add(ctx, bob.Whoami(), roomdb.RoleMember)
r.NoError(err)
// allow bots to dial the remote
// side-effect of re-using a room-server as the client
_, err = bobsMembers.Add(ctx, "the-room", serv.Whoami(), roomdb.RoleMember)
_, err = bobsMembers.Add(ctx, serv.Whoami(), roomdb.RoleMember)
r.NoError(err)
// should work (we allowed A)

View File

@ -83,12 +83,12 @@ func TestTunnelServerSimple(t *testing.T) {
botB := theBots[2].Server
// allow both clients
theBots[0].Members.Add(ctx, "botA", botA.Whoami(), roomdb.RoleMember)
theBots[0].Members.Add(ctx, "botB", botB.Whoami(), roomdb.RoleMember)
theBots[0].Members.Add(ctx, botA.Whoami(), roomdb.RoleMember)
theBots[0].Members.Add(ctx, botB.Whoami(), roomdb.RoleMember)
// allow bots to dial the remote
theBots[1].Members.Add(ctx, "srv", serv.Whoami(), roomdb.RoleMember)
theBots[2].Members.Add(ctx, "srv", serv.Whoami(), roomdb.RoleMember)
theBots[1].Members.Add(ctx, serv.Whoami(), roomdb.RoleMember)
theBots[2].Members.Add(ctx, serv.Whoami(), roomdb.RoleMember)
// dial up B->A and C->A
@ -154,12 +154,12 @@ func TestRoomAnnounce(t *testing.T) {
botB := theBots[2].Server
// allow both clients
theBots[0].Members.Add(ctx, "botA", botA.Whoami(), roomdb.RoleMember)
theBots[0].Members.Add(ctx, "botB", botB.Whoami(), roomdb.RoleMember)
theBots[0].Members.Add(ctx, botA.Whoami(), roomdb.RoleMember)
theBots[0].Members.Add(ctx, botB.Whoami(), roomdb.RoleMember)
// allow bots to dial the remote
theBots[1].Members.Add(ctx, "srv", serv.Whoami(), roomdb.RoleMember)
theBots[2].Members.Add(ctx, "srv", serv.Whoami(), roomdb.RoleMember)
theBots[1].Members.Add(ctx, serv.Whoami(), roomdb.RoleMember)
theBots[2].Members.Add(ctx, serv.Whoami(), roomdb.RoleMember)
// should work (we allowed A)
err := botA.Network.Connect(ctx, serv.Network.GetListenAddr())

View File

@ -23,7 +23,7 @@ func TestGoServerJSClientAliases(t *testing.T) {
var aliasesDB = &mockdb.FakeAliasesService{}
srv := ts.startGoServer(membersDB, aliasesDB)
// allow all peers (there arent any we dont want to allow)
membersDB.GetByFeedReturns(roomdb.Member{Nickname: "free4all"}, nil)
membersDB.GetByFeedReturns(roomdb.Member{ID: 1234}, nil)
// setup mocks for this test
aliasesDB.RegisterReturns(nil)

View File

@ -73,7 +73,7 @@ func TestGoServerLegacyJSClient(t *testing.T) {
var aliases = &mockdb.FakeAliasesService{}
srv := ts.startGoServer(membersDB, aliases)
// allow all peers (there arent any we dont want to allow)
membersDB.GetByFeedReturns(roomdb.Member{Nickname: "free4all"}, nil)
membersDB.GetByFeedReturns(roomdb.Member{ID: 1234}, nil)
alice := ts.startJSClient("alice", "./testscripts/legacy_client.js",
srv.Network.GetListenAddr(),
@ -110,7 +110,7 @@ func TestModernJSClient(t *testing.T) {
var membersDB = &mockdb.FakeMembersService{}
var aliasesDB = &mockdb.FakeAliasesService{}
srv := ts.startGoServer(membersDB, aliasesDB)
membersDB.GetByFeedReturns(roomdb.Member{Nickname: "free4all"}, nil)
membersDB.GetByFeedReturns(roomdb.Member{ID: 1234}, nil)
// allow all peers (there arent any we dont want to allow in this test)

View File

@ -54,7 +54,7 @@ type AuthWithSSBService interface {
// MembersService stores and retreives the list of internal users (members, mods and admins).
type MembersService interface {
// Add adds a new member
Add(_ context.Context, nickName string, pubKey refs.FeedRef, r Role) (int64, error)
Add(_ context.Context, pubKey refs.FeedRef, r Role) (int64, error)
// GetByID returns the member if it exists
GetByID(context.Context, int64) (Member, error)
@ -125,7 +125,7 @@ type InvitesService interface {
// Create creates a new invite for a new member. It returns the token or an error.
// createdBy is user ID of the admin or moderator who created it.
// aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21)
Create(ctx context.Context, createdBy int64, aliasSuggestion string) (string, error)
Create(ctx context.Context, createdBy int64) (string, error)
// Consume checks if the passed token is still valid.
// If it is it adds newMember to the members of the room and invalidates the token.

View File

@ -25,12 +25,11 @@ type FakeInvitesService struct {
result1 roomdb.Invite
result2 error
}
CreateStub func(context.Context, int64, string) (string, error)
CreateStub func(context.Context, int64) (string, error)
createMutex sync.RWMutex
createArgsForCall []struct {
arg1 context.Context
arg2 int64
arg3 string
}
createReturns struct {
result1 string
@ -163,20 +162,19 @@ func (fake *FakeInvitesService) ConsumeReturnsOnCall(i int, result1 roomdb.Invit
}{result1, result2}
}
func (fake *FakeInvitesService) Create(arg1 context.Context, arg2 int64, arg3 string) (string, error) {
func (fake *FakeInvitesService) Create(arg1 context.Context, arg2 int64) (string, error) {
fake.createMutex.Lock()
ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)]
fake.createArgsForCall = append(fake.createArgsForCall, struct {
arg1 context.Context
arg2 int64
arg3 string
}{arg1, arg2, arg3})
}{arg1, arg2})
stub := fake.CreateStub
fakeReturns := fake.createReturns
fake.recordInvocation("Create", []interface{}{arg1, arg2, arg3})
fake.recordInvocation("Create", []interface{}{arg1, arg2})
fake.createMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
@ -190,17 +188,17 @@ func (fake *FakeInvitesService) CreateCallCount() int {
return len(fake.createArgsForCall)
}
func (fake *FakeInvitesService) CreateCalls(stub func(context.Context, int64, string) (string, error)) {
func (fake *FakeInvitesService) CreateCalls(stub func(context.Context, int64) (string, error)) {
fake.createMutex.Lock()
defer fake.createMutex.Unlock()
fake.CreateStub = stub
}
func (fake *FakeInvitesService) CreateArgsForCall(i int) (context.Context, int64, string) {
func (fake *FakeInvitesService) CreateArgsForCall(i int) (context.Context, int64) {
fake.createMutex.RLock()
defer fake.createMutex.RUnlock()
argsForCall := fake.createArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeInvitesService) CreateReturns(result1 string, result2 error) {

View File

@ -10,13 +10,12 @@ import (
)
type FakeMembersService struct {
AddStub func(context.Context, string, refs.FeedRef, roomdb.Role) (int64, error)
AddStub func(context.Context, refs.FeedRef, roomdb.Role) (int64, error)
addMutex sync.RWMutex
addArgsForCall []struct {
arg1 context.Context
arg2 string
arg3 refs.FeedRef
arg4 roomdb.Role
arg2 refs.FeedRef
arg3 roomdb.Role
}
addReturns struct {
result1 int64
@ -108,21 +107,20 @@ type FakeMembersService struct {
invocationsMutex sync.RWMutex
}
func (fake *FakeMembersService) Add(arg1 context.Context, arg2 string, arg3 refs.FeedRef, arg4 roomdb.Role) (int64, error) {
func (fake *FakeMembersService) Add(arg1 context.Context, arg2 refs.FeedRef, arg3 roomdb.Role) (int64, error) {
fake.addMutex.Lock()
ret, specificReturn := fake.addReturnsOnCall[len(fake.addArgsForCall)]
fake.addArgsForCall = append(fake.addArgsForCall, struct {
arg1 context.Context
arg2 string
arg3 refs.FeedRef
arg4 roomdb.Role
}{arg1, arg2, arg3, arg4})
arg2 refs.FeedRef
arg3 roomdb.Role
}{arg1, arg2, arg3})
stub := fake.AddStub
fakeReturns := fake.addReturns
fake.recordInvocation("Add", []interface{}{arg1, arg2, arg3, arg4})
fake.recordInvocation("Add", []interface{}{arg1, arg2, arg3})
fake.addMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3, arg4)
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
@ -136,17 +134,17 @@ func (fake *FakeMembersService) AddCallCount() int {
return len(fake.addArgsForCall)
}
func (fake *FakeMembersService) AddCalls(stub func(context.Context, string, refs.FeedRef, roomdb.Role) (int64, error)) {
func (fake *FakeMembersService) AddCalls(stub func(context.Context, refs.FeedRef, roomdb.Role) (int64, error)) {
fake.addMutex.Lock()
defer fake.addMutex.Unlock()
fake.AddStub = stub
}
func (fake *FakeMembersService) AddArgsForCall(i int) (context.Context, string, refs.FeedRef, roomdb.Role) {
func (fake *FakeMembersService) AddArgsForCall(i int) (context.Context, refs.FeedRef, roomdb.Role) {
fake.addMutex.RLock()
defer fake.addMutex.RUnlock()
argsForCall := fake.addArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeMembersService) AddReturns(result1 int64, result2 error) {

View File

@ -65,7 +65,7 @@ func TestAliases(t *testing.T) {
r.Error(err)
// allow the member
_, err = db.Members.Add(ctx, "flaky's nick", newMember, roomdb.RoleMember)
_, err = db.Members.Add(ctx, newMember, roomdb.RoleMember)
r.NoError(err)
err = db.Aliases.Register(ctx, testName, newMember, testSig)

View File

@ -34,10 +34,9 @@ type Invites struct {
// createdBy is user ID of the admin or moderator who created it.
// aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21)
// The returned token is base64 URL encoded and has inviteTokenLength when decoded.
func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion string) (string, error) {
func (i Invites) Create(ctx context.Context, createdBy int64) (string, error) {
var newInvite = models.Invite{
CreatedBy: createdBy,
AliasSuggestion: aliasSuggestion,
CreatedBy: createdBy,
}
tokenBytes := make([]byte, inviteTokenLength)
@ -106,12 +105,7 @@ func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedR
return err
}
memberNick := time.Now().Format("new-member 2006-01-02")
memberNick += "(invited by:" + entry.R.CreatedByMember.Nick + ")"
if entry.AliasSuggestion != "" {
memberNick = entry.AliasSuggestion
}
_, err = i.members.add(ctx, tx, memberNick, newMember, roomdb.RoleMember)
_, err = i.members.add(ctx, tx, newMember, roomdb.RoleMember)
if err != nil {
return err
}
@ -125,10 +119,8 @@ func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedR
inv.ID = entry.ID
inv.CreatedAt = entry.CreatedAt
inv.AliasSuggestion = entry.AliasSuggestion
inv.CreatedBy.ID = entry.R.CreatedByMember.ID
inv.CreatedBy.Role = roomdb.Role(entry.R.CreatedByMember.Role)
inv.CreatedBy.Nickname = entry.R.CreatedByMember.Nick
return nil
})
@ -162,7 +154,7 @@ func (i Invites) GetByToken(ctx context.Context, token string) (roomdb.Invite, e
}
entry, err := models.Invites(
qm.Where("active = true AND token = ?", ht),
qm.Where("active = true AND hashed_token = ?", ht),
qm.Load("CreatedByMember"),
).One(ctx, i.db)
if err != nil {
@ -174,10 +166,8 @@ func (i Invites) GetByToken(ctx context.Context, token string) (roomdb.Invite, e
inv.ID = entry.ID
inv.CreatedAt = entry.CreatedAt
inv.AliasSuggestion = entry.AliasSuggestion
inv.CreatedBy.ID = entry.R.CreatedByMember.ID
inv.CreatedBy.Role = roomdb.Role(entry.R.CreatedByMember.Role)
inv.CreatedBy.Nickname = entry.R.CreatedByMember.Nick
return inv, nil
}
@ -198,10 +188,8 @@ func (i Invites) GetByID(ctx context.Context, id int64) (roomdb.Invite, error) {
inv.ID = entry.ID
inv.CreatedAt = entry.CreatedAt
inv.AliasSuggestion = entry.AliasSuggestion
inv.CreatedBy.ID = entry.R.CreatedByMember.ID
inv.CreatedBy.Role = roomdb.Role(entry.R.CreatedByMember.Role)
inv.CreatedBy.Nickname = entry.R.CreatedByMember.Nick
return inv, nil
}
@ -224,9 +212,7 @@ func (i Invites) List(ctx context.Context) ([]roomdb.Invite, error) {
var inv roomdb.Invite
inv.ID = e.ID
inv.CreatedAt = e.CreatedAt
inv.AliasSuggestion = e.AliasSuggestion
inv.CreatedBy.ID = e.R.CreatedByMember.ID
inv.CreatedBy.Nickname = e.R.CreatedByMember.Nick
invs[idx] = inv
}

View File

@ -47,13 +47,12 @@ func TestInvites(t *testing.T) {
t.Run("user needs to exist", func(t *testing.T) {
r := require.New(t)
_, err := db.Invites.Create(ctx, 666, "")
_, err := db.Invites.Create(ctx, 666)
r.Error(err, "can't create invite for invalid user")
})
testMemberNick := "test-user"
invitingMember := refs.FeedRef{ID: bytes.Repeat([]byte("ohai"), 8), Algo: refs.RefAlgoFeedSSB1}
mid, err := db.Members.Add(ctx, testMemberNick, invitingMember, roomdb.RoleModerator)
mid, err := db.Members.Add(ctx, invitingMember, roomdb.RoleModerator)
require.NoError(t, err, "failed to create test user")
t.Run("simple create and consume", func(t *testing.T) {
@ -62,7 +61,7 @@ func TestInvites(t *testing.T) {
// i really don't want to do a mocked time functions and rather solve the comment in migration 6 instead
before := time.Now()
tok, err := db.Invites.Create(ctx, mid, "bestie")
tok, err := db.Invites.Create(ctx, mid)
r.NoError(err, "failed to create invite token")
_, err = base64.URLEncoding.DecodeString(tok)
@ -72,16 +71,17 @@ func TestInvites(t *testing.T) {
r.NoError(err, "failed to get list of tokens")
r.Len(lst, 1, "expected 1 invite")
r.Equal("bestie", lst[0].AliasSuggestion)
r.Equal(testMemberNick, lst[0].CreatedBy.Nickname)
r.True(lst[0].CreatedAt.After(before), "expected CreatedAt to be after the start marker")
_, nope := db.Members.GetByFeed(ctx, newMember)
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)
r.NoError(err, "failed to consume the invite")
r.Equal(testMemberNick, inv.CreatedBy.Nickname)
r.NotEqualValues(0, inv.ID, "invite ID unset")
r.True(inv.CreatedAt.After(before), "expected CreatedAt to be after the start marker")
@ -102,16 +102,13 @@ func TestInvites(t *testing.T) {
t.Run("simple create but revoke before use", func(t *testing.T) {
r := require.New(t)
tok, err := db.Invites.Create(ctx, mid, "bestie")
tok, err := db.Invites.Create(ctx, mid)
r.NoError(err, "failed to create invite token")
lst, err := db.Invites.List(ctx)
r.NoError(err, "failed to get list of tokens")
r.Len(lst, 1, "expected 1 invite")
r.Equal("bestie", lst[0].AliasSuggestion)
r.Equal(testMemberNick, lst[0].CreatedBy.Nickname)
err = db.Invites.Revoke(ctx, lst[0].ID)
r.NoError(err, "failed to consume the invite")

View File

@ -20,11 +20,11 @@ type Members struct {
db *sql.DB
}
func (m Members) Add(ctx context.Context, nick string, pubKey refs.FeedRef, role roomdb.Role) (int64, error) {
func (m Members) Add(ctx context.Context, pubKey refs.FeedRef, role roomdb.Role) (int64, error) {
var newID int64
err := transact(m.db, func(tx *sql.Tx) error {
var err error
newID, err = m.add(ctx, tx, nick, pubKey, role)
newID, err = m.add(ctx, tx, pubKey, role)
return err
})
if err != nil {
@ -34,7 +34,7 @@ func (m Members) Add(ctx context.Context, nick string, pubKey refs.FeedRef, role
}
// no receiver name because it needs to use the passed transaction
func (Members) add(ctx context.Context, tx *sql.Tx, nick string, pubKey refs.FeedRef, role roomdb.Role) (int64, error) {
func (Members) add(ctx context.Context, tx *sql.Tx, pubKey refs.FeedRef, role roomdb.Role) (int64, error) {
if err := role.IsValid(); err != nil {
return -1, err
}
@ -44,7 +44,6 @@ func (Members) add(ctx context.Context, tx *sql.Tx, nick string, pubKey refs.Fee
}
var newMember models.Member
newMember.Nick = nick
newMember.PubKey = roomdb.DBFeedRef{FeedRef: pubKey}
newMember.Role = int64(role)
@ -62,10 +61,9 @@ func (m Members) GetByID(ctx context.Context, mid int64) (roomdb.Member, error)
return roomdb.Member{}, err
}
return roomdb.Member{
ID: entry.ID,
Role: roomdb.Role(entry.Role),
Nickname: entry.Nick,
PubKey: entry.PubKey.FeedRef,
ID: entry.ID,
Role: roomdb.Role(entry.Role),
PubKey: entry.PubKey.FeedRef,
}, nil
}
@ -76,10 +74,9 @@ func (m Members) GetByFeed(ctx context.Context, h refs.FeedRef) (roomdb.Member,
return roomdb.Member{}, err
}
return roomdb.Member{
ID: entry.ID,
Role: roomdb.Role(entry.Role),
Nickname: entry.Nick,
PubKey: entry.PubKey.FeedRef,
ID: entry.ID,
Role: roomdb.Role(entry.Role),
PubKey: entry.PubKey.FeedRef,
}, nil
}
@ -93,7 +90,6 @@ func (m Members) List(ctx context.Context) ([]roomdb.Member, error) {
var members = make([]roomdb.Member, len(all))
for i, listEntry := range all {
members[i].ID = listEntry.ID
members[i].Nickname = listEntry.Nick
members[i].Role = roomdb.Role(listEntry.Role)
members[i].PubKey = listEntry.PubKey.FeedRef
}

View File

@ -28,12 +28,12 @@ func TestMembers(t *testing.T) {
// broken feed (unknown algo)
tf := refs.FeedRef{ID: bytes.Repeat([]byte("fooo"), 8), Algo: "nope"}
_, err = db.Members.Add(ctx, "dont-add-me", tf, roomdb.RoleMember)
_, err = db.Members.Add(ctx, tf, roomdb.RoleMember)
r.Error(err)
// looks ok at least
okFeed := refs.FeedRef{ID: bytes.Repeat([]byte("acab"), 8), Algo: refs.RefAlgoFeedSSB1}
mid, err := db.Members.Add(ctx, "should-add-me", okFeed, roomdb.RoleMember)
mid, err := db.Members.Add(ctx, okFeed, roomdb.RoleMember)
r.NoError(err)
sqlDB := db.Members.db
@ -51,7 +51,6 @@ func TestMembers(t *testing.T) {
okMember, err := db.Members.GetByFeed(ctx, okFeed)
r.NoError(err)
r.Equal(okMember.ID, mid)
r.Equal(okMember.Nickname, "should-add-me")
r.Equal(okMember.Role, roomdb.RoleMember)
r.True(okMember.PubKey.Equal(&okFeed))
@ -88,10 +87,10 @@ func TestMembersUnique(t *testing.T) {
require.NoError(t, err)
feedA := refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: refs.RefAlgoFeedSSB1}
_, err = db.Members.Add(ctx, "add-me-first", feedA, roomdb.RoleMember)
_, err = db.Members.Add(ctx, feedA, roomdb.RoleMember)
r.NoError(err)
_, err = db.Members.Add(ctx, "dont-add-me-twice", feedA, roomdb.RoleMember)
_, err = db.Members.Add(ctx, feedA, roomdb.RoleMember)
r.Error(err)
lst, err := db.Members.List(ctx)
@ -114,7 +113,7 @@ func TestMembersByID(t *testing.T) {
require.NoError(t, err)
feedA := refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: refs.RefAlgoFeedSSB1}
_, err = db.Members.Add(ctx, "add-me", feedA, roomdb.RoleMember)
_, err = db.Members.Add(ctx, feedA, roomdb.RoleMember)
r.NoError(err)
lst, err := db.Members.List(ctx)
@ -154,12 +153,12 @@ func TestMembersSetRole(t *testing.T) {
// create two users
feedA := refs.FeedRef{ID: bytes.Repeat([]byte("1"), 32), Algo: refs.RefAlgoFeedSSB1}
idA, err := db.Members.Add(ctx, "user-a", feedA, roomdb.RoleAdmin)
idA, err := db.Members.Add(ctx, feedA, roomdb.RoleAdmin)
r.NoError(err)
t.Log("member A:", idA)
feedB := refs.FeedRef{ID: bytes.Repeat([]byte("2"), 32), Algo: refs.RefAlgoFeedSSB1}
idB, err := db.Members.Add(ctx, "user-b", feedB, roomdb.RoleModerator)
idB, err := db.Members.Add(ctx, feedB, roomdb.RoleModerator)
r.NoError(err)
t.Log("member B:", idB)

View File

@ -3,7 +3,6 @@
CREATE TABLE members (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
role INTEGER NOT NULL, -- member, moderator or admin
nick TEXT NOT NULL, -- a nick name for the user (not an alias)
pub_key TEXT NOT NULL UNIQUE,
CHECK(role > 0)
@ -29,7 +28,6 @@ CREATE TABLE invites (
created_by INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
alias_suggestion TEXT NOT NULL DEFAULT "", -- optional
active boolean NOT NULL DEFAULT TRUE,
FOREIGN KEY ( created_by ) REFERENCES members( "id" ) ON DELETE CASCADE

View File

@ -22,31 +22,28 @@ import (
// Invite is an object representing the database table.
type Invite struct {
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
HashedToken string `boil:"hashed_token" json:"hashed_token" toml:"hashed_token" yaml:"hashed_token"`
CreatedBy int64 `boil:"created_by" json:"created_by" toml:"created_by" yaml:"created_by"`
CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"`
AliasSuggestion string `boil:"alias_suggestion" json:"alias_suggestion" toml:"alias_suggestion" yaml:"alias_suggestion"`
Active bool `boil:"active" json:"active" toml:"active" yaml:"active"`
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
HashedToken string `boil:"hashed_token" json:"hashed_token" toml:"hashed_token" yaml:"hashed_token"`
CreatedBy int64 `boil:"created_by" json:"created_by" toml:"created_by" yaml:"created_by"`
CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"`
Active bool `boil:"active" json:"active" toml:"active" yaml:"active"`
R *inviteR `boil:"-" json:"-" toml:"-" yaml:"-"`
L inviteL `boil:"-" json:"-" toml:"-" yaml:"-"`
}
var InviteColumns = struct {
ID string
HashedToken string
CreatedBy string
CreatedAt string
AliasSuggestion string
Active string
ID string
HashedToken string
CreatedBy string
CreatedAt string
Active string
}{
ID: "id",
HashedToken: "hashed_token",
CreatedBy: "created_by",
CreatedAt: "created_at",
AliasSuggestion: "alias_suggestion",
Active: "active",
ID: "id",
HashedToken: "hashed_token",
CreatedBy: "created_by",
CreatedAt: "created_at",
Active: "active",
}
// Generated where
@ -61,19 +58,17 @@ func (w whereHelperbool) GT(x bool) qm.QueryMod { return qmhelper.Where(w.field
func (w whereHelperbool) GTE(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
var InviteWhere = struct {
ID whereHelperint64
HashedToken whereHelperstring
CreatedBy whereHelperint64
CreatedAt whereHelpertime_Time
AliasSuggestion whereHelperstring
Active whereHelperbool
ID whereHelperint64
HashedToken whereHelperstring
CreatedBy whereHelperint64
CreatedAt whereHelpertime_Time
Active whereHelperbool
}{
ID: whereHelperint64{field: "\"invites\".\"id\""},
HashedToken: whereHelperstring{field: "\"invites\".\"hashed_token\""},
CreatedBy: whereHelperint64{field: "\"invites\".\"created_by\""},
CreatedAt: whereHelpertime_Time{field: "\"invites\".\"created_at\""},
AliasSuggestion: whereHelperstring{field: "\"invites\".\"alias_suggestion\""},
Active: whereHelperbool{field: "\"invites\".\"active\""},
ID: whereHelperint64{field: "\"invites\".\"id\""},
HashedToken: whereHelperstring{field: "\"invites\".\"hashed_token\""},
CreatedBy: whereHelperint64{field: "\"invites\".\"created_by\""},
CreatedAt: whereHelpertime_Time{field: "\"invites\".\"created_at\""},
Active: whereHelperbool{field: "\"invites\".\"active\""},
}
// InviteRels is where relationship names are stored.
@ -97,9 +92,9 @@ func (*inviteR) NewStruct() *inviteR {
type inviteL struct{}
var (
inviteAllColumns = []string{"id", "hashed_token", "created_by", "created_at", "alias_suggestion", "active"}
inviteAllColumns = []string{"id", "hashed_token", "created_by", "created_at", "active"}
inviteColumnsWithoutDefault = []string{}
inviteColumnsWithDefault = []string{"id", "hashed_token", "created_by", "created_at", "alias_suggestion", "active"}
inviteColumnsWithDefault = []string{"id", "hashed_token", "created_by", "created_at", "active"}
invitePrimaryKeyColumns = []string{"id"}
)

View File

@ -25,7 +25,6 @@ import (
type Member struct {
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
Role int64 `boil:"role" json:"role" toml:"role" yaml:"role"`
Nick string `boil:"nick" json:"nick" toml:"nick" yaml:"nick"`
PubKey roomdb.DBFeedRef `boil:"pub_key" json:"pub_key" toml:"pub_key" yaml:"pub_key"`
R *memberR `boil:"-" json:"-" toml:"-" yaml:"-"`
@ -35,12 +34,10 @@ type Member struct {
var MemberColumns = struct {
ID string
Role string
Nick string
PubKey string
}{
ID: "id",
Role: "role",
Nick: "nick",
PubKey: "pub_key",
}
@ -49,12 +46,10 @@ var MemberColumns = struct {
var MemberWhere = struct {
ID whereHelperint64
Role whereHelperint64
Nick whereHelperstring
PubKey whereHelperroomdb_DBFeedRef
}{
ID: whereHelperint64{field: "\"members\".\"id\""},
Role: whereHelperint64{field: "\"members\".\"role\""},
Nick: whereHelperstring{field: "\"members\".\"nick\""},
PubKey: whereHelperroomdb_DBFeedRef{field: "\"members\".\"pub_key\""},
}
@ -88,9 +83,9 @@ func (*memberR) NewStruct() *memberR {
type memberL struct{}
var (
memberAllColumns = []string{"id", "role", "nick", "pub_key"}
memberAllColumns = []string{"id", "role", "pub_key"}
memberColumnsWithoutDefault = []string{}
memberColumnsWithDefault = []string{"id", "role", "nick", "pub_key"}
memberColumnsWithDefault = []string{"id", "role", "pub_key"}
memberPrimaryKeyColumns = []string{"id"}
)

View File

@ -40,7 +40,7 @@ func TestBasic(t *testing.T) {
ctx := context.Background()
feedA := refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: refs.RefAlgoFeedSSB1}
memberID, err := db.Members.Add(ctx, "testNick", feedA, roomdb.RoleMember)
memberID, err := db.Members.Add(ctx, feedA, roomdb.RoleMember)
require.NoError(t, err)
require.NotEqual(t, 0, memberID)

View File

@ -28,10 +28,9 @@ type Alias struct {
// Member holds all the information an internal user of the room has.
type Member struct {
ID int64
Nickname string // a common handle for the user (no-one want's to remember public keys)
Role Role
PubKey refs.FeedRef
ID int64
Role Role
PubKey refs.FeedRef
}
//go:generate go run golang.org/x/tools/cmd/stringer -type=Role
@ -93,8 +92,6 @@ type Invite struct {
CreatedBy Member
CreatedAt time.Time
AliasSuggestion string
}
// ListEntry values are returned by the DenyListServices

View File

@ -59,23 +59,18 @@ func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interf
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(), member.ID, aliasSuggestion)
token, err := h.db.Create(req.Context(), member.ID)
if err != nil {
return nil, err
}
urlTo := web.NewURLTo(router.CompleteApp())
acceptURL := urlTo(router.CompleteInviteAccept, "token", token)
acceptURL.Host = h.domainName
acceptURL.Scheme = "https"
facadeURL := urlTo(router.CompleteInviteFacade, "token", token)
facadeURL.Host = h.domainName
facadeURL.Scheme = "https"
return map[string]interface{}{
"Token": token,
"AccepURL": acceptURL.String(),
"AliasSuggestion": aliasSuggestion,
"FacadeURL": facadeURL.String(),
}, nil
}

View File

@ -22,9 +22,9 @@ func TestInvitesOverview(t *testing.T) {
testUser := roomdb.Member{ID: 23}
lst := []roomdb.Invite{
{ID: 1, CreatedBy: testUser, AliasSuggestion: "foo"},
{ID: 2, CreatedBy: testUser, AliasSuggestion: "bar"},
{ID: 3, CreatedBy: testUser, AliasSuggestion: "baz"},
{ID: 1, CreatedBy: testUser},
{ID: 2, CreatedBy: testUser},
{ID: 3, CreatedBy: testUser},
}
ts.InvitesDB.ListReturns(lst, nil)
@ -42,7 +42,7 @@ func TestInvitesOverview(t *testing.T) {
a.EqualValues(3, html.Find(trSelector).Length()/2, "wrong number of entries on the table (plural)")
lst = []roomdb.Invite{
{ID: 666, CreatedBy: testUser, AliasSuggestion: "single entry"},
{ID: 666, CreatedBy: testUser},
}
ts.InvitesDB.ListReturns(lst, nil)
@ -110,15 +110,12 @@ func TestInvitesCreate(t *testing.T) {
testInvite := "your-fake-test-invite"
ts.InvitesDB.CreateReturns(testInvite, nil)
rec := ts.Client.PostForm(urlRemove.String(), url.Values{
"alias_suggestion": []string{"jerry"},
})
rec := ts.Client.PostForm(urlRemove.String(), url.Values{})
a.Equal(http.StatusOK, rec.Code)
r.Equal(1, ts.InvitesDB.CreateCallCount(), "expected one invites.Create call")
_, userID, aliasSuggestion := ts.InvitesDB.CreateArgsForCall(0)
_, userID := 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")
@ -128,10 +125,10 @@ func TestInvitesCreate(t *testing.T) {
{"#welcome", "AdminInviteCreatedWelcome"},
})
wantURL := urlTo(router.CompleteInviteAccept, "token", testInvite)
wantURL := urlTo(router.CompleteInviteFacade, "token", testInvite)
wantURL.Host = ts.Domain
wantURL.Scheme = "https"
shownLink := doc.Find("#invite-accept-link").Text()
shownLink := doc.Find("#invite-facade-link").Text()
a.Equal(wantURL.String(), shownLink)
}

View File

@ -38,13 +38,6 @@ func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
return
}
memberNick := req.Form.Get("nick")
if memberNick == "" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad nick: %q", memberNick))
return
}
newEntry := req.Form.Get("pub_key")
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
@ -53,7 +46,7 @@ func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
return
}
_, err = h.db.Add(req.Context(), memberNick, *newEntryParsed, roomdb.RoleMember)
_, err = h.db.Add(req.Context(), *newEntryParsed, roomdb.RoleMember)
if err != nil {
code := http.StatusInternalServerError
var aa roomdb.ErrAlreadyAdded

View File

@ -63,22 +63,19 @@ func TestMembersAdd(t *testing.T) {
webassert.ElementsInForm(t, formSelection, []webassert.FormElement{
{Name: "pub_key", Type: "text"},
{Name: "nick", Type: "text"},
})
newKey := "@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519"
addVals := url.Values{
// just any key that looks valid
"pub_key": []string{newKey},
"nick": []string{"test-member"},
}
rec := ts.Client.PostForm(addURL.String(), addVals)
a.Equal(http.StatusFound, rec.Code)
a.Equal(1, ts.MembersDB.AddCallCount())
_, addedNick, addedPubKey, addedRole := ts.MembersDB.AddArgsForCall(0)
_, addedPubKey, addedRole := ts.MembersDB.AddArgsForCall(0)
a.Equal(newKey, addedPubKey.Ref())
a.Equal("test-member", addedNick)
a.Equal(roomdb.RoleMember, addedRole)
}
@ -116,9 +113,9 @@ func TestMembers(t *testing.T) {
a := assert.New(t)
lst := []roomdb.Member{
{ID: 1, Nickname: "one", Role: roomdb.RoleMember, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte{0}, 32), Algo: "fake"}},
{ID: 2, Nickname: "two", Role: roomdb.RoleModerator, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: "test"}},
{ID: 3, Nickname: "three", Role: roomdb.RoleAdmin, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte("acab"), 8), Algo: "true"}},
{ID: 1, Role: roomdb.RoleMember, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte{0}, 32), Algo: "fake"}},
{ID: 2, Role: roomdb.RoleModerator, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: "test"}},
{ID: 3, Role: roomdb.RoleAdmin, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte("acab"), 8), Algo: "true"}},
}
ts.MembersDB.ListReturns(lst, nil)
@ -134,7 +131,7 @@ func TestMembers(t *testing.T) {
a.EqualValues(html.Find("#theList li").Length(), 3)
lst = []roomdb.Member{
{ID: 666, Nickname: "four", Role: roomdb.RoleAdmin, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte{1}, 32), Algo: "one"}},
{ID: 666, Role: roomdb.RoleAdmin, PubKey: refs.FeedRef{ID: bytes.Repeat([]byte{1}, 32), Algo: "one"}},
}
ts.MembersDB.ListReturns(lst, nil)

View File

@ -64,9 +64,8 @@ func newSession(t *testing.T) *testSession {
// fake user
ts.User = roomdb.Member{
ID: 1234,
Nickname: "room mate",
Role: roomdb.RoleModerator,
ID: 1234,
Role: roomdb.RoleModerator,
}
// setup rendering

View File

@ -34,6 +34,7 @@ import (
var HTMLTemplates = []string{
"auth/decide_method.tmpl",
"auth/fallback_sign_in.tmpl",
"auth/withssb_server_start.tmpl",
}

View File

@ -189,7 +189,7 @@ func TestAuthWithSSBClientInitNotConnected(t *testing.T) {
a, r := assert.New(t), require.New(t)
// the client is a member but not connected right now
ts.MembersDB.GetByFeedReturns(roomdb.Member{ID: 1234, Nickname: "test-member"}, nil)
ts.MembersDB.GetByFeedReturns(roomdb.Member{ID: 1234}, nil)
ts.MockedEndpoints.GetEndpointForReturns(nil, false)
client, err := keys.NewKeyPair(nil)
@ -259,7 +259,7 @@ func TestAuthWithSSBClientInitHasClient(t *testing.T) {
payload.ServerID = ts.NetworkInfo.RoomID
// the keypair for our client
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
testMember := roomdb.Member{ID: 1234}
client, err := keys.NewKeyPair(nil)
r.NoError(err)
testMember.PubKey = client.Feed
@ -385,7 +385,7 @@ func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
a, r := assert.New(t), require.New(t)
// the keypair for our client
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
testMember := roomdb.Member{ID: 1234}
client, err := keys.NewKeyPair(nil)
r.NoError(err)
testMember.PubKey = client.Feed
@ -431,8 +431,7 @@ func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
a.Equal("start-http-auth", qry.Get("action"))
a.Equal(serverChallenge, qry.Get("sc"))
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(msaddr, qry.Get("multiserverAddress"))
a.Equal(ts.NetworkInfo.MultiserverAddress(), qry.Get("multiserverAddress"))
qrCode, has := html.Find("#start-auth-qrcode").Attr("src")
a.True(has, "should have the inline image data")
@ -513,7 +512,7 @@ func TestAuthWithSSBServerInitWrongSolution(t *testing.T) {
a, r := assert.New(t), require.New(t)
// the keypair for our client
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
testMember := roomdb.Member{ID: 1234}
client, err := keys.NewKeyPair(nil)
r.NoError(err)
testMember.PubKey = client.Feed

View File

@ -36,11 +36,13 @@ var HTMLTemplates = []string{
"landing/index.tmpl",
"landing/about.tmpl",
"aliases-resolved.html",
"invite/accept.tmpl",
"invite/consumed.tmpl",
"auth/fallback_sign_in.tmpl",
"invite/facade.tmpl",
"notice/list.tmpl",
"notice/show.tmpl",
"error.tmpl",
}
@ -94,7 +96,7 @@ func New(
}),
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
return func(routeName string) bool {
route := router.CompleteApp().Get(routeName)
route := m.Get(routeName)
if route == nil {
return false
}
@ -115,7 +117,7 @@ func New(
if err != nil {
return nil
}
route := router.CompleteApp().GetRoute(router.CompleteNoticeShow)
route := m.GetRoute(router.CompleteNoticeShow)
if route == nil {
return nil
}
@ -220,8 +222,6 @@ func New(
mainMux := &http.ServeMux{}
// start hooking up handlers to the router
var muxrpcHostAndPort = fmt.Sprintf("%s:%d", netInfo.Domain, netInfo.PortMUXRPC)
authWithSSB := roomsAuth.NewWithSSBHandler(
m,
r,
@ -299,13 +299,14 @@ func New(
m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve)
var ih = inviteHandler{
render: r,
invites: dbs.Invites,
roomPubKey: netInfo.RoomID.PubKey(),
muxrpcHostAndPort: muxrpcHostAndPort,
networkInfo: netInfo,
}
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.Get(router.CompleteInviteFacade).Handler(r.HTML("invite/facade.tmpl", ih.presentFacade))
m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume)
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
@ -317,11 +318,27 @@ func New(
mainMux.Handle("/", m)
urlTo := web.NewURLTo(m)
consumeURL := urlTo(router.CompleteInviteConsume)
// apply HTTP middleware
middlewares := []func(http.Handler) http.Handler{
logging.InjectHandler(logger),
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
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 {

View File

@ -1,30 +1,35 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html/template"
"net/http"
"go.mindeco.de/logging"
"golang.org/x/crypto/ed25519"
"net/url"
"github.com/go-kit/kit/log/level"
"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/web"
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"
)
type inviteHandler struct {
invites roomdb.InvitesService
aliases roomdb.AliasesService
render *render.Renderer
muxrpcHostAndPort string
roomPubKey ed25519.PublicKey
invites roomdb.InvitesService
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")
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 map[string]interface{}{
"Token": token,
"Invite": inv,
var joinRoomURI url.URL
joinRoomURI.Scheme = "ssb"
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),
"Invite": inv,
"Token": token,
"JoinRoomURI": template.URL(joinRoomURI.String()),
}, 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}
type inviteConsumePayload struct {
ID refs.FeedRef `json:"id"`
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")
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)
inv, err := h.invites.Consume(req.Context(), token, newMember)
if err != nil {
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())
level.Info(log).Log("event", "invite consumed", "id", inv.ID, "ref", newMember.ShortRef())
if alias != "" {
level.Warn(log).Log(
"TODO", "invite registration",
"alias", alias,
)
resp.SendSuccess()
}
// 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
roomPubKey := base64.StdEncoding.EncodeToString(h.roomPubKey)
roomAddr := fmt.Sprintf("net:%s~shs:%s:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", h.muxrpcHostAndPort, roomPubKey)
return map[string]interface{}{
"RoomAddress": roomAddr,
}, nil
err := html.renderer.Render(html.rw, html.req, "invite/consumed.tmpl", http.StatusOK, struct {
SSBURI template.URL
}{template.URL(ssbURI.String())})
if err != nil {
logger := logging.FromContext(html.req.Context())
level.Warn(logger).Log("event", "render failed", "err", err)
}
}
func (html inviteConsumeHTMLResponder) SendError(err error) {
html.renderer.Error(html.rw, html.req, http.StatusInternalServerError, err)
}

View File

@ -3,6 +3,7 @@ package handlers
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/cookiejar"
"net/url"
@ -10,16 +11,15 @@ import (
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"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/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/webassert"
refs "go.mindeco.de/ssb-refs"
)
func TestInviteShowAcceptForm(t *testing.T) {
@ -31,7 +31,7 @@ func TestInviteShowAcceptForm(t *testing.T) {
a, r := assert.New(t), require.New(t)
testToken := "nonexistant-test-token"
acceptURL404 := urlTo(router.CompleteInviteAccept, "token", testToken)
acceptURL404 := urlTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(acceptURL404)
// prep the mocked db for http:404
@ -65,14 +65,11 @@ func TestInviteShowAcceptForm(t *testing.T) {
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token"
validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken)
validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(validAcceptURL)
// prep the mocked db for http:200
fakeExistingInvite := roomdb.Invite{
ID: 1234,
AliasSuggestion: "bestie",
}
fakeExistingInvite := roomdb.Invite{ID: 1234}
ts.InvitesDB.GetByTokenReturns(fakeExistingInvite, nil)
// request the form
@ -87,8 +84,8 @@ func TestInviteShowAcceptForm(t *testing.T) {
a.Equal(testToken, tokenFromArg)
webassert.Localized(t, doc, []webassert.LocalizedElement{
{"#welcome", "InviteAcceptWelcome"},
{"title", "InviteAcceptTitle"},
{"#welcome", "InviteFacadeWelcome"},
{"title", "InviteFacadeTitle"},
})
form := doc.Find("form#consume")
@ -97,9 +94,8 @@ func TestInviteShowAcceptForm(t *testing.T) {
webassert.CSRFTokenPresent(t, form)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "token", Type: "hidden", Value: testToken},
{Name: "alias", Type: "text", Value: fakeExistingInvite.AliasSuggestion},
{Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},
{Name: "invite", Type: "hidden", Value: testToken},
{Name: "id", Type: "text", Placeholder: wantNewMemberPlaceholder},
})
})
@ -107,11 +103,11 @@ func TestInviteShowAcceptForm(t *testing.T) {
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token-2"
validAcceptURL := urlTo(router.CompleteInviteAccept, "token", testToken)
validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(validAcceptURL)
inviteWithNoAlias := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil)
testInvite := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
// request the form
validAcceptForm := validAcceptURL.String()
@ -125,8 +121,8 @@ func TestInviteShowAcceptForm(t *testing.T) {
a.Equal(testToken, tokenFromArg)
webassert.Localized(t, doc, []webassert.LocalizedElement{
{"#welcome", "InviteAcceptWelcome"},
{"title", "InviteAcceptTitle"},
{"#welcome", "InviteFacadeWelcome"},
{"title", "InviteFacadeTitle"},
})
form := doc.Find("form#consume")
@ -134,26 +130,25 @@ func TestInviteShowAcceptForm(t *testing.T) {
webassert.CSRFTokenPresent(t, form)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "token", Type: "hidden", Value: testToken},
{Name: "alias", Type: "text", Placeholder: "you@this.room"},
{Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},
{Name: "invite", Type: "hidden", Value: testToken},
{Name: "id", Type: "text", Placeholder: wantNewMemberPlaceholder},
})
})
}
func TestInviteConsumeInvite(t *testing.T) {
func TestInviteConsumeInviteHTTP(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.CompleteInviteAccept, "token", testToken)
validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(validAcceptURL)
validAcceptURL.Host = "localhost"
validAcceptURL.Scheme = "https"
inviteWithNoAlias := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(inviteWithNoAlias, nil)
testInvite := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
// request the form (for a valid csrf token)
validAcceptForm := validAcceptURL.String()
@ -184,8 +179,8 @@ func TestInviteConsumeInvite(t *testing.T) {
Algo: refs.RefAlgoFeedSSB1,
}
consumeVals := url.Values{
"token": []string{testToken},
"new_member": []string{testNewMember.Ref()},
"invite": []string{testToken},
"id": []string{testNewMember.Ref()},
csrfName: []string{csrfValue},
}
@ -207,7 +202,7 @@ func TestInviteConsumeInvite(t *testing.T) {
ts.Client.SetHeaders(csrfCookieHeader)
// prepare the mock
ts.InvitesDB.ConsumeReturns(inviteWithNoAlias, nil)
ts.InvitesDB.ConsumeReturns(testInvite, nil)
// send the POST
resp = ts.Client.PostForm(consumeInviteURL.String(), consumeVals)
@ -222,11 +217,71 @@ func TestInviteConsumeInvite(t *testing.T) {
consumedDoc, err := goquery.NewDocumentFromReader(resp.Body)
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.Contains(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)
a.True(strings.HasSuffix(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key 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)
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 := urlTo(router.CompleteInviteConsume)
r.NotNil(consumeInviteURL)
// 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)
}

View File

@ -121,10 +121,7 @@ func TestNoticesEditButtonVisible(t *testing.T) {
}
// have the database return okay for any user
testUser := roomdb.Member{
ID: 23,
Nickname: "test admin",
}
testUser := roomdb.Member{ID: 23}
ts.AuthFallbackDB.CheckReturns(testUser.ID, nil)
ts.MembersDB.GetByIDReturns(testUser, nil)

View File

@ -81,11 +81,11 @@ 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:"
InviteAcceptPublicKey = "Public Key"
InviteFacade = "Join Room"
InviteFacadeTitle = "Join Room"
InviteFacadeWelcome = "elaborate welcome message for a new member with good words and stuff."
InviteFacadeAliasSuggestion = "The persone who created thought you might like this alias:"
InviteFacadePublicKey = "Public Key"
InviteConsumedTitle = "Invite accepted!"
InviteConsumedWelcome = "Even more elaborate message that the person is now a member of the room!"

View File

@ -16,7 +16,7 @@ const (
CompleteAliasResolve = "complete:alias:resolve"
CompleteInviteAccept = "complete:invite:accept"
CompleteInviteFacade = "complete:invite:accept"
CompleteInviteConsume = "complete:invite:consume"
)
@ -32,7 +32,7 @@ func CompleteApp() *mux.Router {
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("/notice/show").Methods("GET").Name(CompleteNoticeShow)

View File

@ -7,17 +7,6 @@
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}}
<pre id="invite-facade-link">{{.FacadeURL}}</pre>
</div>
{{end}}

View File

@ -42,7 +42,6 @@
<tr class="hidden sm:table-row h-8 uppercase text-sm text-gray-400">
<th class="w-10 text-center pl-3 pr-6">{{i18n "AdminInvitesCreatedAtColumn"}}</th>
<th class="w-1/2 text-left px-6">{{i18n "AdminInvitesCreatorColumn"}}</th>
<th class="w-1/2 text-left px-6">{{i18n "AdminInvitesAliasColumn"}}</th>
<th class="w-0"></th>
</tr>
</thead>
@ -55,8 +54,6 @@
<span class="tooltip">{{.CreatedAt.Format "2006-01-02T15:04:05.00"}}</span>
</div>
</td>
<td class="px-6">{{.CreatedBy.Nickname}}</td>
<td class="px-6">{{if ne .AliasSuggestion ""}}{{.AliasSuggestion}}{{end}}</td>
<td class="pl-2 pr-3">
<a
href="{{urlTo "admin:invites:revoke:confirm" "id" .ID}}"
@ -66,7 +63,7 @@
</tr>
<tr class="h-12 table-row sm:hidden">
<td class="flex flex-row items-center" colspan="4">
<span class="flex-1">{{i18n "AdminInvitesSummaryFrom"}} <b>{{.CreatedBy.Nickname}}</b> {{i18n "AdminInvitesSummaryTo"}} <b>{{if ne .AliasSuggestion ""}}{{.AliasSuggestion}}{{else}}?{{end}}</b></span>
<span class="flex-1">{{i18n "AdminInvitesSummaryFrom"}} <b>{{.CreatedBy.PubKey.Ref }}</b> {{i18n "AdminInvitesSummaryTo"}}</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"

View File

@ -9,7 +9,7 @@
<pre
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 ""}}
<!-- https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60 -->

View File

@ -19,12 +19,6 @@
>
{{ .csrfField }}
<div class="flex flex-row items-center h-12">
<input
type="text"
name="nick"
placeholder="member nickname"
class="font-mono truncate w-1/2 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="text"
name="pub_key"
@ -40,9 +34,6 @@
</form>
{{range $index, $member := .Entries}}
<li class="flex flex-row items-center h-12">
<span
class="font-mono truncate flex-auto text-gray-600 tracking-wider w-1/3"
>{{$member.Nickname}}</span>
<form
class="change-member-role"

View File

@ -31,7 +31,7 @@
<svg class="text-green-500 w-4 h-4 mr-1" viewBox="0 0 24 24">
<path fill="currentColor" d="M22,18V22H18V19H15V16H12L9.74,13.74C9.19,13.91 8.61,14 8,14A6,6 0 0,1 2,8A6,6 0 0,1 8,2A6,6 0 0,1 14,8C14,8.61 13.91,9.19 13.74,9.74L22,18M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5Z" />
</svg>
<span class="text-green-500 text-sm truncate w-32">{{$user.Nickname}} {{$user.PubKey.Ref}}</span>
<span class="text-green-500 text-sm truncate w-32">{{$user.PubKey.Ref}}</span>
</div>
<a
href="{{urlTo "auth:logout"}}"

View File

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

View File

@ -7,7 +7,6 @@
class="text-center"
>{{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>
<pre id="room-address">{{.RoomAddress}}</pre>
<a id="join-link" href="{{.SSBURI}}">Join Room</a>
</div>
{{end}}

View File

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