add admindb.InviteService

interface methods: create, consume, list and revoke.

SQLite implementation and some light testing.

Related changes:

* have authfallback.Create return the user id

At some point we will need to not assume that authfallback is our users
table but that will not become relevant before we start adding
moderation roles.

* Update package documentation of admindb and admindb/sqlite

* remove leftover generated.db

now using the roomdb file created by TestSimple

Review comments by @cblgh

* better documentation of hashed token storage
* space between %d and `bytes`
* make interface assertion comments less scary
This commit is contained in:
Henry 2021-03-02 17:14:02 +01:00 committed by Henry
parent 96a30af405
commit bbcab73cb5
23 changed files with 2018 additions and 38 deletions

View File

@ -1,5 +1,13 @@
// SPDX-License-Identifier: MIT
// Package admindb implements all the persisted database needs of the room server.
// This includes authentication, allow/deny list managment, invite and alias creation and also the notice content for the CMS.
//
// The interfaces defined here are implemented twice. Once in SQLite for production and once as mocks for testing, generated by counterfeiter (https://github.com/maxbrunsfeld/counterfeiter).
//
// See the package documentation of admindb/sqlite for how to update it.
// It's important not to use the types generated by sqlboiler (sqlite/models) in the argument and return values of the interfaces here.
// This would leak details of the internal implementation of the admindb/sqlite package and we want to have full control over how these interfaces can be used.
package admindb
import (
@ -13,7 +21,7 @@ import (
type AuthFallbackService interface {
auth.Auther
Create(ctx context.Context, user string, password []byte) error
Create(ctx context.Context, user string, password []byte) (int64, error)
GetByID(ctx context.Context, uid int64) (*User, error)
}
@ -47,6 +55,25 @@ type AllowListService interface {
// AliasService manages alias handle registration and lookup
type AliasService interface{}
// InviteService manages creation and consumption of invite tokens for joining the room.
type InviteService 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)
// 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.
// If the token isn't valid, it returns an error.
Consume(ctx context.Context, token string, newMember refs.FeedRef) (Invite, error)
// List returns a list of all the valid invites
List(ctx context.Context) ([]Invite, error)
// Revoke removes a active invite and invalidates it for future use.
Revoke(ctx context.Context, id int64) error
}
// PinnedNoticesService allows an admin to assign Notices to specific placeholder pages.
// like updates, privacy policy, code of conduct
type PinnedNoticesService interface {
@ -82,6 +109,8 @@ type NoticesService interface {
//go:generate counterfeiter -o mockdb/alias.go . AliasService
//go:generate counterfeiter -o mockdb/invite.go . InviteService
//go:generate counterfeiter -o mockdb/fixed_pages.go . PinnedNoticesService
//go:generate counterfeiter -o mockdb/pages.go . NoticesService

View File

@ -23,7 +23,7 @@ type FakeAuthFallbackService struct {
result1 interface{}
result2 error
}
CreateStub func(context.Context, string, []byte) error
CreateStub func(context.Context, string, []byte) (int64, error)
createMutex sync.RWMutex
createArgsForCall []struct {
arg1 context.Context
@ -31,10 +31,12 @@ type FakeAuthFallbackService struct {
arg3 []byte
}
createReturns struct {
result1 error
result1 int64
result2 error
}
createReturnsOnCall map[int]struct {
result1 error
result1 int64
result2 error
}
GetByIDStub func(context.Context, int64) (*admindb.User, error)
getByIDMutex sync.RWMutex
@ -119,7 +121,7 @@ func (fake *FakeAuthFallbackService) CheckReturnsOnCall(i int, result1 interface
}{result1, result2}
}
func (fake *FakeAuthFallbackService) Create(arg1 context.Context, arg2 string, arg3 []byte) error {
func (fake *FakeAuthFallbackService) Create(arg1 context.Context, arg2 string, arg3 []byte) (int64, error) {
var arg3Copy []byte
if arg3 != nil {
arg3Copy = make([]byte, len(arg3))
@ -140,9 +142,9 @@ func (fake *FakeAuthFallbackService) Create(arg1 context.Context, arg2 string, a
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
return ret.result1, ret.result2
}
return fakeReturns.result1
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeAuthFallbackService) CreateCallCount() int {
@ -151,7 +153,7 @@ func (fake *FakeAuthFallbackService) CreateCallCount() int {
return len(fake.createArgsForCall)
}
func (fake *FakeAuthFallbackService) CreateCalls(stub func(context.Context, string, []byte) error) {
func (fake *FakeAuthFallbackService) CreateCalls(stub func(context.Context, string, []byte) (int64, error)) {
fake.createMutex.Lock()
defer fake.createMutex.Unlock()
fake.CreateStub = stub
@ -164,27 +166,30 @@ func (fake *FakeAuthFallbackService) CreateArgsForCall(i int) (context.Context,
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeAuthFallbackService) CreateReturns(result1 error) {
func (fake *FakeAuthFallbackService) CreateReturns(result1 int64, result2 error) {
fake.createMutex.Lock()
defer fake.createMutex.Unlock()
fake.CreateStub = nil
fake.createReturns = struct {
result1 error
}{result1}
result1 int64
result2 error
}{result1, result2}
}
func (fake *FakeAuthFallbackService) CreateReturnsOnCall(i int, result1 error) {
func (fake *FakeAuthFallbackService) CreateReturnsOnCall(i int, result1 int64, result2 error) {
fake.createMutex.Lock()
defer fake.createMutex.Unlock()
fake.CreateStub = nil
if fake.createReturnsOnCall == nil {
fake.createReturnsOnCall = make(map[int]struct {
result1 error
result1 int64
result2 error
})
}
fake.createReturnsOnCall[i] = struct {
result1 error
}{result1}
result1 int64
result2 error
}{result1, result2}
}
func (fake *FakeAuthFallbackService) GetByID(arg1 context.Context, arg2 int64) (*admindb.User, error) {

360
admindb/mockdb/invite.go Normal file
View File

@ -0,0 +1,360 @@
// Code generated by counterfeiter. DO NOT EDIT.
package mockdb
import (
"context"
"sync"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
refs "go.mindeco.de/ssb-refs"
)
type FakeInviteService struct {
ConsumeStub func(context.Context, string, refs.FeedRef) (admindb.Invite, error)
consumeMutex sync.RWMutex
consumeArgsForCall []struct {
arg1 context.Context
arg2 string
arg3 refs.FeedRef
}
consumeReturns struct {
result1 admindb.Invite
result2 error
}
consumeReturnsOnCall map[int]struct {
result1 admindb.Invite
result2 error
}
CreateStub func(context.Context, int64, string) (string, error)
createMutex sync.RWMutex
createArgsForCall []struct {
arg1 context.Context
arg2 int64
arg3 string
}
createReturns struct {
result1 string
result2 error
}
createReturnsOnCall map[int]struct {
result1 string
result2 error
}
ListStub func(context.Context) ([]admindb.Invite, error)
listMutex sync.RWMutex
listArgsForCall []struct {
arg1 context.Context
}
listReturns struct {
result1 []admindb.Invite
result2 error
}
listReturnsOnCall map[int]struct {
result1 []admindb.Invite
result2 error
}
RevokeStub func(context.Context, int64) error
revokeMutex sync.RWMutex
revokeArgsForCall []struct {
arg1 context.Context
arg2 int64
}
revokeReturns struct {
result1 error
}
revokeReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeInviteService) Consume(arg1 context.Context, arg2 string, arg3 refs.FeedRef) (admindb.Invite, error) {
fake.consumeMutex.Lock()
ret, specificReturn := fake.consumeReturnsOnCall[len(fake.consumeArgsForCall)]
fake.consumeArgsForCall = append(fake.consumeArgsForCall, struct {
arg1 context.Context
arg2 string
arg3 refs.FeedRef
}{arg1, arg2, arg3})
stub := fake.ConsumeStub
fakeReturns := fake.consumeReturns
fake.recordInvocation("Consume", []interface{}{arg1, arg2, arg3})
fake.consumeMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeInviteService) ConsumeCallCount() int {
fake.consumeMutex.RLock()
defer fake.consumeMutex.RUnlock()
return len(fake.consumeArgsForCall)
}
func (fake *FakeInviteService) ConsumeCalls(stub func(context.Context, string, refs.FeedRef) (admindb.Invite, error)) {
fake.consumeMutex.Lock()
defer fake.consumeMutex.Unlock()
fake.ConsumeStub = stub
}
func (fake *FakeInviteService) ConsumeArgsForCall(i int) (context.Context, string, refs.FeedRef) {
fake.consumeMutex.RLock()
defer fake.consumeMutex.RUnlock()
argsForCall := fake.consumeArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeInviteService) ConsumeReturns(result1 admindb.Invite, result2 error) {
fake.consumeMutex.Lock()
defer fake.consumeMutex.Unlock()
fake.ConsumeStub = nil
fake.consumeReturns = struct {
result1 admindb.Invite
result2 error
}{result1, result2}
}
func (fake *FakeInviteService) ConsumeReturnsOnCall(i int, result1 admindb.Invite, result2 error) {
fake.consumeMutex.Lock()
defer fake.consumeMutex.Unlock()
fake.ConsumeStub = nil
if fake.consumeReturnsOnCall == nil {
fake.consumeReturnsOnCall = make(map[int]struct {
result1 admindb.Invite
result2 error
})
}
fake.consumeReturnsOnCall[i] = struct {
result1 admindb.Invite
result2 error
}{result1, result2}
}
func (fake *FakeInviteService) Create(arg1 context.Context, arg2 int64, arg3 string) (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})
stub := fake.CreateStub
fakeReturns := fake.createReturns
fake.recordInvocation("Create", []interface{}{arg1, arg2, arg3})
fake.createMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeInviteService) CreateCallCount() int {
fake.createMutex.RLock()
defer fake.createMutex.RUnlock()
return len(fake.createArgsForCall)
}
func (fake *FakeInviteService) CreateCalls(stub func(context.Context, int64, string) (string, error)) {
fake.createMutex.Lock()
defer fake.createMutex.Unlock()
fake.CreateStub = stub
}
func (fake *FakeInviteService) CreateArgsForCall(i int) (context.Context, int64, string) {
fake.createMutex.RLock()
defer fake.createMutex.RUnlock()
argsForCall := fake.createArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeInviteService) CreateReturns(result1 string, result2 error) {
fake.createMutex.Lock()
defer fake.createMutex.Unlock()
fake.CreateStub = nil
fake.createReturns = struct {
result1 string
result2 error
}{result1, result2}
}
func (fake *FakeInviteService) CreateReturnsOnCall(i int, result1 string, result2 error) {
fake.createMutex.Lock()
defer fake.createMutex.Unlock()
fake.CreateStub = nil
if fake.createReturnsOnCall == nil {
fake.createReturnsOnCall = make(map[int]struct {
result1 string
result2 error
})
}
fake.createReturnsOnCall[i] = struct {
result1 string
result2 error
}{result1, result2}
}
func (fake *FakeInviteService) List(arg1 context.Context) ([]admindb.Invite, error) {
fake.listMutex.Lock()
ret, specificReturn := fake.listReturnsOnCall[len(fake.listArgsForCall)]
fake.listArgsForCall = append(fake.listArgsForCall, struct {
arg1 context.Context
}{arg1})
stub := fake.ListStub
fakeReturns := fake.listReturns
fake.recordInvocation("List", []interface{}{arg1})
fake.listMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeInviteService) ListCallCount() int {
fake.listMutex.RLock()
defer fake.listMutex.RUnlock()
return len(fake.listArgsForCall)
}
func (fake *FakeInviteService) ListCalls(stub func(context.Context) ([]admindb.Invite, error)) {
fake.listMutex.Lock()
defer fake.listMutex.Unlock()
fake.ListStub = stub
}
func (fake *FakeInviteService) ListArgsForCall(i int) context.Context {
fake.listMutex.RLock()
defer fake.listMutex.RUnlock()
argsForCall := fake.listArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeInviteService) ListReturns(result1 []admindb.Invite, result2 error) {
fake.listMutex.Lock()
defer fake.listMutex.Unlock()
fake.ListStub = nil
fake.listReturns = struct {
result1 []admindb.Invite
result2 error
}{result1, result2}
}
func (fake *FakeInviteService) ListReturnsOnCall(i int, result1 []admindb.Invite, result2 error) {
fake.listMutex.Lock()
defer fake.listMutex.Unlock()
fake.ListStub = nil
if fake.listReturnsOnCall == nil {
fake.listReturnsOnCall = make(map[int]struct {
result1 []admindb.Invite
result2 error
})
}
fake.listReturnsOnCall[i] = struct {
result1 []admindb.Invite
result2 error
}{result1, result2}
}
func (fake *FakeInviteService) Revoke(arg1 context.Context, arg2 int64) error {
fake.revokeMutex.Lock()
ret, specificReturn := fake.revokeReturnsOnCall[len(fake.revokeArgsForCall)]
fake.revokeArgsForCall = append(fake.revokeArgsForCall, struct {
arg1 context.Context
arg2 int64
}{arg1, arg2})
stub := fake.RevokeStub
fakeReturns := fake.revokeReturns
fake.recordInvocation("Revoke", []interface{}{arg1, arg2})
fake.revokeMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeInviteService) RevokeCallCount() int {
fake.revokeMutex.RLock()
defer fake.revokeMutex.RUnlock()
return len(fake.revokeArgsForCall)
}
func (fake *FakeInviteService) RevokeCalls(stub func(context.Context, int64) error) {
fake.revokeMutex.Lock()
defer fake.revokeMutex.Unlock()
fake.RevokeStub = stub
}
func (fake *FakeInviteService) RevokeArgsForCall(i int) (context.Context, int64) {
fake.revokeMutex.RLock()
defer fake.revokeMutex.RUnlock()
argsForCall := fake.revokeArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeInviteService) RevokeReturns(result1 error) {
fake.revokeMutex.Lock()
defer fake.revokeMutex.Unlock()
fake.RevokeStub = nil
fake.revokeReturns = struct {
result1 error
}{result1}
}
func (fake *FakeInviteService) RevokeReturnsOnCall(i int, result1 error) {
fake.revokeMutex.Lock()
defer fake.revokeMutex.Unlock()
fake.RevokeStub = nil
if fake.revokeReturnsOnCall == nil {
fake.revokeReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.revokeReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeInviteService) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.consumeMutex.RLock()
defer fake.consumeMutex.RUnlock()
fake.createMutex.RLock()
defer fake.createMutex.RUnlock()
fake.listMutex.RLock()
defer fake.listMutex.RUnlock()
fake.revokeMutex.RLock()
defer fake.revokeMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeInviteService) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ admindb.InviteService = new(FakeInviteService)

View File

@ -8,7 +8,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
)
// make sure to implement interfaces correctly
// compiler assertion to ensure the struct fullfills the interface
var _ admindb.AliasService = (*Aliases)(nil)
type Aliases struct {

View File

@ -17,7 +17,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
)
// make sure to implement interfaces correctly
// compiler assertion to ensure the struct fullfills the interface
var _ admindb.AuthFallbackService = (*AuthFallback)(nil)
type AuthFallback struct {
@ -39,23 +39,23 @@ func (ah AuthFallback) Check(name, password string) (interface{}, error) {
return found.ID, nil
}
func (ah AuthFallback) Create(ctx context.Context, name string, password []byte) error {
func (ah AuthFallback) Create(ctx context.Context, name string, password []byte) (int64, error) {
var u models.AuthFallback
u.Name = name
hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("auth/fallback: failed to hash password for new user")
return -1, fmt.Errorf("auth/fallback: failed to hash password for new user")
}
u.PasswordHash = hashed
err = u.Insert(ctx, ah.db, boil.Infer())
if err != nil {
return fmt.Errorf("auth/fallback: failed to insert new user: %w", err)
return -1, fmt.Errorf("auth/fallback: failed to insert new user: %w", err)
}
return nil
return u.ID, nil
}
func (ah AuthFallback) GetByID(ctx context.Context, uid int64) (*admindb.User, error) {

View File

@ -8,7 +8,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
)
// make sure to implement interfaces correctly
// compiler assertion to ensure the struct fullfills the interface
var _ admindb.AuthWithSSBService = (*AuthWithSSB)(nil)
type AuthWithSSB struct {

View File

@ -2,6 +2,20 @@
set -e
go test
# ensure tools are installed
go get github.com/volatiletech/sqlboiler/v4
go get github.com/volatiletech/sqlboiler-sqlite3
# run the migrations (creates testrun/TestSimple/roomdb)
go test -run Simple
# make sure the sqlite file was created
test -f testrun/TestSimple/roomdb || {
echo 'roomdb file missing'
exit 1
}
# generate the models package
sqlboiler sqlite3 --wipe --no-tests
echo "all done!"
echo "all done. models updated!"

Binary file not shown.

207
admindb/sqlite/invites.go Normal file
View File

@ -0,0 +1,207 @@
package sqlite
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/base64"
"fmt"
"github.com/friendsofgo/errors"
"github.com/mattn/go-sqlite3"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb/sqlite/models"
refs "go.mindeco.de/ssb-refs"
)
// compiler assertion to ensure the struct fullfills the interface
var _ admindb.InviteService = (*Invites)(nil)
// Invites implements the admindb.InviteService.
// Tokens are stored as sha256 hashes on disk to protect against attackers gaining database read-access.
type Invites struct {
db *sql.DB
allowList *AllowList
}
const tokenLength = 50
// 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)
// The returned token is base64 URL encoded and has tokenLength when decoded.
func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion string) (string, error) {
var newInvite = models.Invite{
CreatedBy: createdBy,
AliasSuggestion: aliasSuggestion,
}
tokenBytes := make([]byte, tokenLength)
err := transact(i.db, func(tx *sql.Tx) error {
inserted := false
trying:
for tries := 100; tries > 0; tries-- {
// generate an invite code
rand.Read(tokenBytes)
// hash the binary of the token for storage
h := sha256.New()
h.Write(tokenBytes)
newInvite.Token = fmt.Sprintf("%x", h.Sum(nil))
// insert the new invite
err := newInvite.Insert(ctx, tx, boil.Infer())
if err != nil {
var sqlErr sqlite3.Error
if errors.As(err, &sqlErr) && sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique {
// generated an existing token, retry
continue trying
}
return err
}
inserted = true
break // no error means it worked!
}
if !inserted {
return errors.New("admindb: failed to generate an invite token in a reasonable amount of time")
}
return nil
})
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(tokenBytes), nil
}
// 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.
// If the token isn't valid, it returns an error.
// Tokens need to be base64 URL encoded and when decoded be of tokenLength.
func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedRef) (admindb.Invite, error) {
var inv admindb.Invite
tokenBytes, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return inv, err
}
if n := len(tokenBytes); n != tokenLength {
return inv, fmt.Errorf("admindb: invalid invite token length (only got %d bytes)", n)
}
// hash the binary of the passed token
h := sha256.New()
h.Write(tokenBytes)
hashedToken := fmt.Sprintf("%x", h.Sum(nil))
err = transact(i.db, func(tx *sql.Tx) error {
entry, err := models.Invites(
qm.Where("active = true AND token = ?", hashedToken),
qm.Load("CreatedByAuthFallback"),
).One(ctx, tx)
if err != nil {
return err
}
err = i.allowList.add(ctx, tx, newMember)
if err != nil {
return err
}
// invalidate the invite for consumption
entry.Active = false
_, err = entry.Update(ctx, tx, boil.Whitelist("active"))
if err != nil {
return err
}
inv.ID = entry.ID
inv.AliasSuggestion = entry.AliasSuggestion
inv.CreatedBy.ID = entry.R.CreatedByAuthFallback.ID
inv.CreatedBy.Name = entry.R.CreatedByAuthFallback.Name
return nil
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return inv, admindb.ErrNotFound
}
return inv, err
}
return inv, nil
}
// since invites are marked as inavalid so that the code can't be generated twice,
// they need to be deleted periodically.
func deleteConsumedInvites(tx boil.ContextExecutor) error {
_, err := models.Invites(qm.Where("active = false")).DeleteAll(context.Background(), tx)
if err != nil {
return fmt.Errorf("admindb: failed to delete used invites: %w", err)
}
return nil
}
// List returns a list of all the valid invites
func (i Invites) List(ctx context.Context) ([]admindb.Invite, error) {
var invs []admindb.Invite
err := transact(i.db, func(tx *sql.Tx) error {
entries, err := models.Invites(
qm.Where("active = true"),
qm.Load("CreatedByAuthFallback"),
).All(ctx, tx)
if err != nil {
return err
}
invs = make([]admindb.Invite, len(entries))
for i, e := range entries {
var inv admindb.Invite
inv.ID = e.ID
inv.AliasSuggestion = e.AliasSuggestion
inv.CreatedBy.ID = e.R.CreatedByAuthFallback.ID
inv.CreatedBy.Name = e.R.CreatedByAuthFallback.Name
invs[i] = inv
}
return nil
})
if err != nil {
return nil, err
}
return invs, nil
}
// Revoke removes a active invite and invalidates it for future use.
func (i Invites) Revoke(ctx context.Context, id int64) error {
return transact(i.db, func(tx *sql.Tx) error {
entry, err := models.Invites(
qm.Where("active = true AND id = ?", id),
).One(ctx, tx)
if err != nil {
return err
}
entry.Active = false
_, err = entry.Update(ctx, tx, boil.Whitelist("active"))
if err != nil {
return err
}
return nil
})
}

View File

@ -0,0 +1,110 @@
package sqlite
import (
"bytes"
"context"
"encoding/base64"
"math/rand"
"os"
"path/filepath"
"testing"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/stretchr/testify/require"
refs "go.mindeco.de/ssb-refs"
)
func TestInvites(t *testing.T) {
ctx := context.Background()
testRepo := filepath.Join("testrun", t.Name())
os.RemoveAll(testRepo)
tr := repo.New(testRepo)
// fake feed for testing, looks ok at least
newMember := refs.FeedRef{ID: bytes.Repeat([]byte("acab"), 8), Algo: refs.RefAlgoFeedSSB1}
db, err := Open(tr)
require.NoError(t, err)
t.Run("try to consume invalid token", func(t *testing.T) {
r := require.New(t)
lst, err := db.Invites.List(ctx)
r.NoError(err, "failed to get empty list of tokens")
r.Len(lst, 0, "expected no active invites")
randToken := make([]byte, 32)
rand.Read(randToken)
_, err = db.Invites.Consume(ctx, string(randToken), newMember)
r.Error(err, "expected error for inactive invite")
})
t.Run("user needs to exist", func(t *testing.T) {
r := require.New(t)
_, err := db.Invites.Create(ctx, 666, "")
r.Error(err, "can't create invite for invalid user")
})
testUserName := "test-user"
uid, err := db.AuthFallback.Create(ctx, testUserName, []byte("bad-password"))
require.NoError(t, err, "failed to create test user")
t.Run("simple create and consume", func(t *testing.T) {
r := require.New(t)
tok, err := db.Invites.Create(ctx, uid, "bestie")
r.NoError(err, "failed to create invite token")
_, err = base64.URLEncoding.DecodeString(tok)
r.NoError(err, "not a valid base64 string")
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(testUserName, lst[0].CreatedBy.Name)
inv, err := db.Invites.Consume(ctx, tok, newMember)
r.NoError(err, "failed to consume the invite")
r.Equal(testUserName, inv.CreatedBy.Name)
r.NotEqualValues(0, inv.ID, "invite ID unset")
lst, err = db.Invites.List(ctx)
r.NoError(err, "failed to get list of tokens post consume")
r.Len(lst, 0, "expected no active invites")
// can't use twice
_, err = db.Invites.Consume(ctx, tok, newMember)
r.Error(err, "failed to consume the invite")
})
t.Run("simple create but revoke before use", func(t *testing.T) {
r := require.New(t)
tok, err := db.Invites.Create(ctx, uid, "bestie")
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(testUserName, lst[0].CreatedBy.Name)
err = db.Invites.Revoke(ctx, lst[0].ID)
r.NoError(err, "failed to consume the invite")
lst, err = db.Invites.List(ctx)
r.NoError(err, "failed to get list of tokens post consume")
r.Len(lst, 0, "expected no active invites")
// can't use twice
_, err = db.Invites.Consume(ctx, tok, newMember)
r.Error(err, "failed to consume the invite")
})
}

View File

@ -0,0 +1,22 @@
-- +migrate Up
CREATE TABLE invites (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
token text UNIQUE NOT NULL,
created_by integer NOT NULL,
alias_suggestion text NOT NULL DEFAULT "", -- optional
active boolean NOT NULL DEFAULT true,
-- TODO: replace auth_fallback with a user table once we do "sign in with ssb"
FOREIGN KEY ( created_by ) REFERENCES auth_fallback( "id" )
);
CREATE INDEX invite_active_ids ON invites(id) WHERE active=true;
CREATE UNIQUE INDEX invite_active_tokens ON invites(token) WHERE active=true;
CREATE INDEX invite_inactive ON invites(active);
-- +migrate Down
DROP TABLE invites;
DROP INDEX invite_active_ids;
DROP INDEX invite_active_tokens;
DROP INDEX invite_inactive;

View File

@ -86,10 +86,14 @@ var AuthFallbackWhere = struct {
// AuthFallbackRels is where relationship names are stored.
var AuthFallbackRels = struct {
}{}
CreatedByInvites string
}{
CreatedByInvites: "CreatedByInvites",
}
// authFallbackR is where relationships are stored.
type authFallbackR struct {
CreatedByInvites InviteSlice `boil:"CreatedByInvites" json:"CreatedByInvites" toml:"CreatedByInvites" yaml:"CreatedByInvites"`
}
// NewStruct creates a new relationship struct
@ -382,6 +386,178 @@ func (q authFallbackQuery) Exists(ctx context.Context, exec boil.ContextExecutor
return count > 0, nil
}
// CreatedByInvites retrieves all the invite's Invites with an executor via created_by column.
func (o *AuthFallback) CreatedByInvites(mods ...qm.QueryMod) inviteQuery {
var queryMods []qm.QueryMod
if len(mods) != 0 {
queryMods = append(queryMods, mods...)
}
queryMods = append(queryMods,
qm.Where("\"invites\".\"created_by\"=?", o.ID),
)
query := Invites(queryMods...)
queries.SetFrom(query.Query, "\"invites\"")
if len(queries.GetSelect(query.Query)) == 0 {
queries.SetSelect(query.Query, []string{"\"invites\".*"})
}
return query
}
// LoadCreatedByInvites allows an eager lookup of values, cached into the
// loaded structs of the objects. This is for a 1-M or N-M relationship.
func (authFallbackL) LoadCreatedByInvites(ctx context.Context, e boil.ContextExecutor, singular bool, maybeAuthFallback interface{}, mods queries.Applicator) error {
var slice []*AuthFallback
var object *AuthFallback
if singular {
object = maybeAuthFallback.(*AuthFallback)
} else {
slice = *maybeAuthFallback.(*[]*AuthFallback)
}
args := make([]interface{}, 0, 1)
if singular {
if object.R == nil {
object.R = &authFallbackR{}
}
args = append(args, object.ID)
} else {
Outer:
for _, obj := range slice {
if obj.R == nil {
obj.R = &authFallbackR{}
}
for _, a := range args {
if a == obj.ID {
continue Outer
}
}
args = append(args, obj.ID)
}
}
if len(args) == 0 {
return nil
}
query := NewQuery(
qm.From(`invites`),
qm.WhereIn(`invites.created_by in ?`, args...),
)
if mods != nil {
mods.Apply(query)
}
results, err := query.QueryContext(ctx, e)
if err != nil {
return errors.Wrap(err, "failed to eager load invites")
}
var resultSlice []*Invite
if err = queries.Bind(results, &resultSlice); err != nil {
return errors.Wrap(err, "failed to bind eager loaded slice invites")
}
if err = results.Close(); err != nil {
return errors.Wrap(err, "failed to close results in eager load on invites")
}
if err = results.Err(); err != nil {
return errors.Wrap(err, "error occurred during iteration of eager loaded relations for invites")
}
if len(inviteAfterSelectHooks) != 0 {
for _, obj := range resultSlice {
if err := obj.doAfterSelectHooks(ctx, e); err != nil {
return err
}
}
}
if singular {
object.R.CreatedByInvites = resultSlice
for _, foreign := range resultSlice {
if foreign.R == nil {
foreign.R = &inviteR{}
}
foreign.R.CreatedByAuthFallback = object
}
return nil
}
for _, foreign := range resultSlice {
for _, local := range slice {
if local.ID == foreign.CreatedBy {
local.R.CreatedByInvites = append(local.R.CreatedByInvites, foreign)
if foreign.R == nil {
foreign.R = &inviteR{}
}
foreign.R.CreatedByAuthFallback = local
break
}
}
}
return nil
}
// AddCreatedByInvites adds the given related objects to the existing relationships
// of the auth_fallback, optionally inserting them as new records.
// Appends related to o.R.CreatedByInvites.
// Sets related.R.CreatedByAuthFallback appropriately.
func (o *AuthFallback) AddCreatedByInvites(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*Invite) error {
var err error
for _, rel := range related {
if insert {
rel.CreatedBy = o.ID
if err = rel.Insert(ctx, exec, boil.Infer()); err != nil {
return errors.Wrap(err, "failed to insert into foreign table")
}
} else {
updateQuery := fmt.Sprintf(
"UPDATE \"invites\" SET %s WHERE %s",
strmangle.SetParamNames("\"", "\"", 0, []string{"created_by"}),
strmangle.WhereClause("\"", "\"", 0, invitePrimaryKeyColumns),
)
values := []interface{}{o.ID, rel.ID}
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, updateQuery)
fmt.Fprintln(writer, values)
}
if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
return errors.Wrap(err, "failed to update foreign table")
}
rel.CreatedBy = o.ID
}
}
if o.R == nil {
o.R = &authFallbackR{
CreatedByInvites: related,
}
} else {
o.R.CreatedByInvites = append(o.R.CreatedByInvites, related...)
}
for _, rel := range related {
if rel.R == nil {
rel.R = &inviteR{
CreatedByAuthFallback: o,
}
} else {
rel.R.CreatedByAuthFallback = o
}
}
return nil
}
// AuthFallbacks retrieves all the records using an executor.
func AuthFallbacks(mods ...qm.QueryMod) authFallbackQuery {
mods = append(mods, qm.From("\"auth_fallback\""))

View File

@ -6,12 +6,14 @@ package models
var TableNames = struct {
AllowList string
AuthFallback string
Invites string
Notices string
PinNotices string
Pins string
}{
AllowList: "allow_list",
AuthFallback: "auth_fallback",
Invites: "invites",
Notices: "notices",
PinNotices: "pin_notices",
Pins: "pins",

View File

@ -0,0 +1,972 @@
// Code generated by SQLBoiler 4.4.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"context"
"database/sql"
"fmt"
"reflect"
"strings"
"sync"
"time"
"github.com/friendsofgo/errors"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
"github.com/volatiletech/sqlboiler/v4/queries/qmhelper"
"github.com/volatiletech/strmangle"
)
// Invite is an object representing the database table.
type Invite struct {
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
Token string `boil:"token" json:"token" toml:"token" yaml:"token"`
CreatedBy int64 `boil:"created_by" json:"created_by" toml:"created_by" yaml:"created_by"`
AliasSuggestion string `boil:"alias_suggestion" json:"alias_suggestion" toml:"alias_suggestion" yaml:"alias_suggestion"`
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
Token string
CreatedBy string
AliasSuggestion string
Active string
}{
ID: "id",
Token: "token",
CreatedBy: "created_by",
AliasSuggestion: "alias_suggestion",
Active: "active",
}
// Generated where
type whereHelperbool struct{ field string }
func (w whereHelperbool) EQ(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
func (w whereHelperbool) NEQ(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) }
func (w whereHelperbool) LT(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) }
func (w whereHelperbool) LTE(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) }
func (w whereHelperbool) GT(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) }
func (w whereHelperbool) GTE(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
var InviteWhere = struct {
ID whereHelperint64
Token whereHelperstring
CreatedBy whereHelperint64
AliasSuggestion whereHelperstring
Active whereHelperbool
}{
ID: whereHelperint64{field: "\"invites\".\"id\""},
Token: whereHelperstring{field: "\"invites\".\"token\""},
CreatedBy: whereHelperint64{field: "\"invites\".\"created_by\""},
AliasSuggestion: whereHelperstring{field: "\"invites\".\"alias_suggestion\""},
Active: whereHelperbool{field: "\"invites\".\"active\""},
}
// InviteRels is where relationship names are stored.
var InviteRels = struct {
CreatedByAuthFallback string
}{
CreatedByAuthFallback: "CreatedByAuthFallback",
}
// inviteR is where relationships are stored.
type inviteR struct {
CreatedByAuthFallback *AuthFallback `boil:"CreatedByAuthFallback" json:"CreatedByAuthFallback" toml:"CreatedByAuthFallback" yaml:"CreatedByAuthFallback"`
}
// NewStruct creates a new relationship struct
func (*inviteR) NewStruct() *inviteR {
return &inviteR{}
}
// inviteL is where Load methods for each relationship are stored.
type inviteL struct{}
var (
inviteAllColumns = []string{"id", "token", "created_by", "alias_suggestion", "active"}
inviteColumnsWithoutDefault = []string{}
inviteColumnsWithDefault = []string{"id", "token", "created_by", "alias_suggestion", "active"}
invitePrimaryKeyColumns = []string{"id"}
)
type (
// InviteSlice is an alias for a slice of pointers to Invite.
// This should generally be used opposed to []Invite.
InviteSlice []*Invite
// InviteHook is the signature for custom Invite hook methods
InviteHook func(context.Context, boil.ContextExecutor, *Invite) error
inviteQuery struct {
*queries.Query
}
)
// Cache for insert, update and upsert
var (
inviteType = reflect.TypeOf(&Invite{})
inviteMapping = queries.MakeStructMapping(inviteType)
invitePrimaryKeyMapping, _ = queries.BindMapping(inviteType, inviteMapping, invitePrimaryKeyColumns)
inviteInsertCacheMut sync.RWMutex
inviteInsertCache = make(map[string]insertCache)
inviteUpdateCacheMut sync.RWMutex
inviteUpdateCache = make(map[string]updateCache)
inviteUpsertCacheMut sync.RWMutex
inviteUpsertCache = make(map[string]insertCache)
)
var (
// Force time package dependency for automated UpdatedAt/CreatedAt.
_ = time.Second
// Force qmhelper dependency for where clause generation (which doesn't
// always happen)
_ = qmhelper.Where
)
var inviteBeforeInsertHooks []InviteHook
var inviteBeforeUpdateHooks []InviteHook
var inviteBeforeDeleteHooks []InviteHook
var inviteBeforeUpsertHooks []InviteHook
var inviteAfterInsertHooks []InviteHook
var inviteAfterSelectHooks []InviteHook
var inviteAfterUpdateHooks []InviteHook
var inviteAfterDeleteHooks []InviteHook
var inviteAfterUpsertHooks []InviteHook
// doBeforeInsertHooks executes all "before insert" hooks.
func (o *Invite) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteBeforeInsertHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doBeforeUpdateHooks executes all "before Update" hooks.
func (o *Invite) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteBeforeUpdateHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doBeforeDeleteHooks executes all "before Delete" hooks.
func (o *Invite) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteBeforeDeleteHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doBeforeUpsertHooks executes all "before Upsert" hooks.
func (o *Invite) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteBeforeUpsertHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doAfterInsertHooks executes all "after Insert" hooks.
func (o *Invite) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteAfterInsertHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doAfterSelectHooks executes all "after Select" hooks.
func (o *Invite) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteAfterSelectHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doAfterUpdateHooks executes all "after Update" hooks.
func (o *Invite) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteAfterUpdateHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doAfterDeleteHooks executes all "after Delete" hooks.
func (o *Invite) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteAfterDeleteHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// doAfterUpsertHooks executes all "after Upsert" hooks.
func (o *Invite) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
if boil.HooksAreSkipped(ctx) {
return nil
}
for _, hook := range inviteAfterUpsertHooks {
if err := hook(ctx, exec, o); err != nil {
return err
}
}
return nil
}
// AddInviteHook registers your hook function for all future operations.
func AddInviteHook(hookPoint boil.HookPoint, inviteHook InviteHook) {
switch hookPoint {
case boil.BeforeInsertHook:
inviteBeforeInsertHooks = append(inviteBeforeInsertHooks, inviteHook)
case boil.BeforeUpdateHook:
inviteBeforeUpdateHooks = append(inviteBeforeUpdateHooks, inviteHook)
case boil.BeforeDeleteHook:
inviteBeforeDeleteHooks = append(inviteBeforeDeleteHooks, inviteHook)
case boil.BeforeUpsertHook:
inviteBeforeUpsertHooks = append(inviteBeforeUpsertHooks, inviteHook)
case boil.AfterInsertHook:
inviteAfterInsertHooks = append(inviteAfterInsertHooks, inviteHook)
case boil.AfterSelectHook:
inviteAfterSelectHooks = append(inviteAfterSelectHooks, inviteHook)
case boil.AfterUpdateHook:
inviteAfterUpdateHooks = append(inviteAfterUpdateHooks, inviteHook)
case boil.AfterDeleteHook:
inviteAfterDeleteHooks = append(inviteAfterDeleteHooks, inviteHook)
case boil.AfterUpsertHook:
inviteAfterUpsertHooks = append(inviteAfterUpsertHooks, inviteHook)
}
}
// One returns a single invite record from the query.
func (q inviteQuery) One(ctx context.Context, exec boil.ContextExecutor) (*Invite, error) {
o := &Invite{}
queries.SetLimit(q.Query, 1)
err := q.Bind(ctx, exec, o)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, errors.Wrap(err, "models: failed to execute a one query for invites")
}
if err := o.doAfterSelectHooks(ctx, exec); err != nil {
return o, err
}
return o, nil
}
// All returns all Invite records from the query.
func (q inviteQuery) All(ctx context.Context, exec boil.ContextExecutor) (InviteSlice, error) {
var o []*Invite
err := q.Bind(ctx, exec, &o)
if err != nil {
return nil, errors.Wrap(err, "models: failed to assign all query results to Invite slice")
}
if len(inviteAfterSelectHooks) != 0 {
for _, obj := range o {
if err := obj.doAfterSelectHooks(ctx, exec); err != nil {
return o, err
}
}
}
return o, nil
}
// Count returns the count of all Invite records in the query.
func (q inviteQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
var count int64
queries.SetSelect(q.Query, nil)
queries.SetCount(q.Query)
err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
if err != nil {
return 0, errors.Wrap(err, "models: failed to count invites rows")
}
return count, nil
}
// Exists checks if the row exists in the table.
func (q inviteQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) {
var count int64
queries.SetSelect(q.Query, nil)
queries.SetCount(q.Query)
queries.SetLimit(q.Query, 1)
err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
if err != nil {
return false, errors.Wrap(err, "models: failed to check if invites exists")
}
return count > 0, nil
}
// CreatedByAuthFallback pointed to by the foreign key.
func (o *Invite) CreatedByAuthFallback(mods ...qm.QueryMod) authFallbackQuery {
queryMods := []qm.QueryMod{
qm.Where("\"id\" = ?", o.CreatedBy),
}
queryMods = append(queryMods, mods...)
query := AuthFallbacks(queryMods...)
queries.SetFrom(query.Query, "\"auth_fallback\"")
return query
}
// LoadCreatedByAuthFallback allows an eager lookup of values, cached into the
// loaded structs of the objects. This is for an N-1 relationship.
func (inviteL) LoadCreatedByAuthFallback(ctx context.Context, e boil.ContextExecutor, singular bool, maybeInvite interface{}, mods queries.Applicator) error {
var slice []*Invite
var object *Invite
if singular {
object = maybeInvite.(*Invite)
} else {
slice = *maybeInvite.(*[]*Invite)
}
args := make([]interface{}, 0, 1)
if singular {
if object.R == nil {
object.R = &inviteR{}
}
args = append(args, object.CreatedBy)
} else {
Outer:
for _, obj := range slice {
if obj.R == nil {
obj.R = &inviteR{}
}
for _, a := range args {
if a == obj.CreatedBy {
continue Outer
}
}
args = append(args, obj.CreatedBy)
}
}
if len(args) == 0 {
return nil
}
query := NewQuery(
qm.From(`auth_fallback`),
qm.WhereIn(`auth_fallback.id in ?`, args...),
)
if mods != nil {
mods.Apply(query)
}
results, err := query.QueryContext(ctx, e)
if err != nil {
return errors.Wrap(err, "failed to eager load AuthFallback")
}
var resultSlice []*AuthFallback
if err = queries.Bind(results, &resultSlice); err != nil {
return errors.Wrap(err, "failed to bind eager loaded slice AuthFallback")
}
if err = results.Close(); err != nil {
return errors.Wrap(err, "failed to close results of eager load for auth_fallback")
}
if err = results.Err(); err != nil {
return errors.Wrap(err, "error occurred during iteration of eager loaded relations for auth_fallback")
}
if len(inviteAfterSelectHooks) != 0 {
for _, obj := range resultSlice {
if err := obj.doAfterSelectHooks(ctx, e); err != nil {
return err
}
}
}
if len(resultSlice) == 0 {
return nil
}
if singular {
foreign := resultSlice[0]
object.R.CreatedByAuthFallback = foreign
if foreign.R == nil {
foreign.R = &authFallbackR{}
}
foreign.R.CreatedByInvites = append(foreign.R.CreatedByInvites, object)
return nil
}
for _, local := range slice {
for _, foreign := range resultSlice {
if local.CreatedBy == foreign.ID {
local.R.CreatedByAuthFallback = foreign
if foreign.R == nil {
foreign.R = &authFallbackR{}
}
foreign.R.CreatedByInvites = append(foreign.R.CreatedByInvites, local)
break
}
}
}
return nil
}
// SetCreatedByAuthFallback of the invite to the related item.
// Sets o.R.CreatedByAuthFallback to related.
// Adds o to related.R.CreatedByInvites.
func (o *Invite) SetCreatedByAuthFallback(ctx context.Context, exec boil.ContextExecutor, insert bool, related *AuthFallback) error {
var err error
if insert {
if err = related.Insert(ctx, exec, boil.Infer()); err != nil {
return errors.Wrap(err, "failed to insert into foreign table")
}
}
updateQuery := fmt.Sprintf(
"UPDATE \"invites\" SET %s WHERE %s",
strmangle.SetParamNames("\"", "\"", 0, []string{"created_by"}),
strmangle.WhereClause("\"", "\"", 0, invitePrimaryKeyColumns),
)
values := []interface{}{related.ID, o.ID}
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, updateQuery)
fmt.Fprintln(writer, values)
}
if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
return errors.Wrap(err, "failed to update local table")
}
o.CreatedBy = related.ID
if o.R == nil {
o.R = &inviteR{
CreatedByAuthFallback: related,
}
} else {
o.R.CreatedByAuthFallback = related
}
if related.R == nil {
related.R = &authFallbackR{
CreatedByInvites: InviteSlice{o},
}
} else {
related.R.CreatedByInvites = append(related.R.CreatedByInvites, o)
}
return nil
}
// Invites retrieves all the records using an executor.
func Invites(mods ...qm.QueryMod) inviteQuery {
mods = append(mods, qm.From("\"invites\""))
return inviteQuery{NewQuery(mods...)}
}
// FindInvite retrieves a single record by ID with an executor.
// If selectCols is empty Find will return all columns.
func FindInvite(ctx context.Context, exec boil.ContextExecutor, iD int64, selectCols ...string) (*Invite, error) {
inviteObj := &Invite{}
sel := "*"
if len(selectCols) > 0 {
sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",")
}
query := fmt.Sprintf(
"select %s from \"invites\" where \"id\"=?", sel,
)
q := queries.Raw(query, iD)
err := q.Bind(ctx, exec, inviteObj)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, errors.Wrap(err, "models: unable to select from invites")
}
return inviteObj, nil
}
// Insert a single record using an executor.
// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts.
func (o *Invite) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error {
if o == nil {
return errors.New("models: no invites provided for insertion")
}
var err error
if err := o.doBeforeInsertHooks(ctx, exec); err != nil {
return err
}
nzDefaults := queries.NonZeroDefaultSet(inviteColumnsWithDefault, o)
key := makeCacheKey(columns, nzDefaults)
inviteInsertCacheMut.RLock()
cache, cached := inviteInsertCache[key]
inviteInsertCacheMut.RUnlock()
if !cached {
wl, returnColumns := columns.InsertColumnSet(
inviteAllColumns,
inviteColumnsWithDefault,
inviteColumnsWithoutDefault,
nzDefaults,
)
cache.valueMapping, err = queries.BindMapping(inviteType, inviteMapping, wl)
if err != nil {
return err
}
cache.retMapping, err = queries.BindMapping(inviteType, inviteMapping, returnColumns)
if err != nil {
return err
}
if len(wl) != 0 {
cache.query = fmt.Sprintf("INSERT INTO \"invites\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1))
} else {
cache.query = "INSERT INTO \"invites\" %sDEFAULT VALUES%s"
}
var queryOutput, queryReturning string
if len(cache.retMapping) != 0 {
cache.retQuery = fmt.Sprintf("SELECT \"%s\" FROM \"invites\" WHERE %s", strings.Join(returnColumns, "\",\""), strmangle.WhereClause("\"", "\"", 0, invitePrimaryKeyColumns))
}
cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning)
}
value := reflect.Indirect(reflect.ValueOf(o))
vals := queries.ValuesFromMapping(value, cache.valueMapping)
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, cache.query)
fmt.Fprintln(writer, vals)
}
result, err := exec.ExecContext(ctx, cache.query, vals...)
if err != nil {
return errors.Wrap(err, "models: unable to insert into invites")
}
var lastID int64
var identifierCols []interface{}
if len(cache.retMapping) == 0 {
goto CacheNoHooks
}
lastID, err = result.LastInsertId()
if err != nil {
return ErrSyncFail
}
o.ID = int64(lastID)
if lastID != 0 && len(cache.retMapping) == 1 && cache.retMapping[0] == inviteMapping["id"] {
goto CacheNoHooks
}
identifierCols = []interface{}{
o.ID,
}
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, cache.retQuery)
fmt.Fprintln(writer, identifierCols...)
}
err = exec.QueryRowContext(ctx, cache.retQuery, identifierCols...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...)
if err != nil {
return errors.Wrap(err, "models: unable to populate default values for invites")
}
CacheNoHooks:
if !cached {
inviteInsertCacheMut.Lock()
inviteInsertCache[key] = cache
inviteInsertCacheMut.Unlock()
}
return o.doAfterInsertHooks(ctx, exec)
}
// Update uses an executor to update the Invite.
// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates.
// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records.
func (o *Invite) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) {
var err error
if err = o.doBeforeUpdateHooks(ctx, exec); err != nil {
return 0, err
}
key := makeCacheKey(columns, nil)
inviteUpdateCacheMut.RLock()
cache, cached := inviteUpdateCache[key]
inviteUpdateCacheMut.RUnlock()
if !cached {
wl := columns.UpdateColumnSet(
inviteAllColumns,
invitePrimaryKeyColumns,
)
if !columns.IsWhitelist() {
wl = strmangle.SetComplement(wl, []string{"created_at"})
}
if len(wl) == 0 {
return 0, errors.New("models: unable to update invites, could not build whitelist")
}
cache.query = fmt.Sprintf("UPDATE \"invites\" SET %s WHERE %s",
strmangle.SetParamNames("\"", "\"", 0, wl),
strmangle.WhereClause("\"", "\"", 0, invitePrimaryKeyColumns),
)
cache.valueMapping, err = queries.BindMapping(inviteType, inviteMapping, append(wl, invitePrimaryKeyColumns...))
if err != nil {
return 0, err
}
}
values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping)
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, cache.query)
fmt.Fprintln(writer, values)
}
var result sql.Result
result, err = exec.ExecContext(ctx, cache.query, values...)
if err != nil {
return 0, errors.Wrap(err, "models: unable to update invites row")
}
rowsAff, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "models: failed to get rows affected by update for invites")
}
if !cached {
inviteUpdateCacheMut.Lock()
inviteUpdateCache[key] = cache
inviteUpdateCacheMut.Unlock()
}
return rowsAff, o.doAfterUpdateHooks(ctx, exec)
}
// UpdateAll updates all rows with the specified column values.
func (q inviteQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
queries.SetUpdate(q.Query, cols)
result, err := q.Query.ExecContext(ctx, exec)
if err != nil {
return 0, errors.Wrap(err, "models: unable to update all for invites")
}
rowsAff, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "models: unable to retrieve rows affected for invites")
}
return rowsAff, nil
}
// UpdateAll updates all rows with the specified column values, using an executor.
func (o InviteSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
ln := int64(len(o))
if ln == 0 {
return 0, nil
}
if len(cols) == 0 {
return 0, errors.New("models: update all requires at least one column argument")
}
colNames := make([]string, len(cols))
args := make([]interface{}, len(cols))
i := 0
for name, value := range cols {
colNames[i] = name
args[i] = value
i++
}
// Append all of the primary key values for each column
for _, obj := range o {
pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), invitePrimaryKeyMapping)
args = append(args, pkeyArgs...)
}
sql := fmt.Sprintf("UPDATE \"invites\" SET %s WHERE %s",
strmangle.SetParamNames("\"", "\"", 0, colNames),
strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, invitePrimaryKeyColumns, len(o)))
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, sql)
fmt.Fprintln(writer, args...)
}
result, err := exec.ExecContext(ctx, sql, args...)
if err != nil {
return 0, errors.Wrap(err, "models: unable to update all in invite slice")
}
rowsAff, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all invite")
}
return rowsAff, nil
}
// Delete deletes a single Invite record with an executor.
// Delete will match against the primary key column to find the record to delete.
func (o *Invite) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
if o == nil {
return 0, errors.New("models: no Invite provided for delete")
}
if err := o.doBeforeDeleteHooks(ctx, exec); err != nil {
return 0, err
}
args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), invitePrimaryKeyMapping)
sql := "DELETE FROM \"invites\" WHERE \"id\"=?"
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, sql)
fmt.Fprintln(writer, args...)
}
result, err := exec.ExecContext(ctx, sql, args...)
if err != nil {
return 0, errors.Wrap(err, "models: unable to delete from invites")
}
rowsAff, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "models: failed to get rows affected by delete for invites")
}
if err := o.doAfterDeleteHooks(ctx, exec); err != nil {
return 0, err
}
return rowsAff, nil
}
// DeleteAll deletes all matching rows.
func (q inviteQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
if q.Query == nil {
return 0, errors.New("models: no inviteQuery provided for delete all")
}
queries.SetDelete(q.Query)
result, err := q.Query.ExecContext(ctx, exec)
if err != nil {
return 0, errors.Wrap(err, "models: unable to delete all from invites")
}
rowsAff, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for invites")
}
return rowsAff, nil
}
// DeleteAll deletes all rows in the slice, using an executor.
func (o InviteSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
if len(o) == 0 {
return 0, nil
}
if len(inviteBeforeDeleteHooks) != 0 {
for _, obj := range o {
if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil {
return 0, err
}
}
}
var args []interface{}
for _, obj := range o {
pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), invitePrimaryKeyMapping)
args = append(args, pkeyArgs...)
}
sql := "DELETE FROM \"invites\" WHERE " +
strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, invitePrimaryKeyColumns, len(o))
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, sql)
fmt.Fprintln(writer, args)
}
result, err := exec.ExecContext(ctx, sql, args...)
if err != nil {
return 0, errors.Wrap(err, "models: unable to delete all from invite slice")
}
rowsAff, err := result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for invites")
}
if len(inviteAfterDeleteHooks) != 0 {
for _, obj := range o {
if err := obj.doAfterDeleteHooks(ctx, exec); err != nil {
return 0, err
}
}
}
return rowsAff, nil
}
// Reload refetches the object from the database
// using the primary keys with an executor.
func (o *Invite) Reload(ctx context.Context, exec boil.ContextExecutor) error {
ret, err := FindInvite(ctx, exec, o.ID)
if err != nil {
return err
}
*o = *ret
return nil
}
// ReloadAll refetches every row with matching primary key column values
// and overwrites the original object slice with the newly updated slice.
func (o *InviteSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error {
if o == nil || len(*o) == 0 {
return nil
}
slice := InviteSlice{}
var args []interface{}
for _, obj := range *o {
pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), invitePrimaryKeyMapping)
args = append(args, pkeyArgs...)
}
sql := "SELECT \"invites\".* FROM \"invites\" WHERE " +
strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, invitePrimaryKeyColumns, len(*o))
q := queries.Raw(sql, args...)
err := q.Bind(ctx, exec, &slice)
if err != nil {
return errors.Wrap(err, "models: unable to reload all in InviteSlice")
}
*o = slice
return nil
}
// InviteExists checks if the Invite row exists.
func InviteExists(ctx context.Context, exec boil.ContextExecutor, iD int64) (bool, error) {
var exists bool
sql := "select exists(select 1 from \"invites\" where \"id\"=? limit 1)"
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, sql)
fmt.Fprintln(writer, iD)
}
row := exec.QueryRowContext(ctx, sql, iD)
err := row.Scan(&exists)
if err != nil {
return false, errors.Wrap(err, "models: unable to check if invites exists")
}
return exists, nil
}

View File

@ -1,5 +1,19 @@
// SPDX-License-Identifier: MIT
// Package sqlite implements the SQLite backend of the admindb interfaces.
//
// It uses sql-migrate (github.com/rubenv/sql-migrate) for it's schema definition and maintainace.
// For query construction/ORM it uses SQLBoiler (https://github.com/volatiletech/sqlboiler).
//
// The process of updating the schema and ORM can be summarized as follows:
//
// 1. Make changes to the interfaces in package admindb
// 2. Add a new migration to the 'migrations' folder
// 3. Run 'go test -run Simple', which applies all the migrations
// 4. Run sqlboiler to generate package models
// 5. Implement the interface as needed by using the models package
//
// For convenience step 3 and 4 are combined in the generate_models bash script.
package sqlite
import (
@ -8,6 +22,7 @@ import (
"log"
"os"
"path/filepath"
"time"
migrate "github.com/rubenv/sql-migrate"
@ -26,6 +41,8 @@ type Database struct {
PinnedNotices admindb.PinnedNoticesService
Notices admindb.NoticesService
Invites admindb.InviteService
}
// Open looks for a database file 'fname'
@ -39,6 +56,9 @@ func Open(r repo.Interface) (*Database, error) {
}
}
// enable constraint enforcment for relations
fname += "?_foreign_keys=on"
db, err := sql.Open("sqlite3", fname)
if err != nil {
return nil, fmt.Errorf("admindb: failed to open sqlite database: %w", err)
@ -57,14 +77,38 @@ func Open(r repo.Interface) (*Database, error) {
log.Printf("admindb: applied %d migrations", n)
}
if err := deleteConsumedInvites(db); err != nil {
return nil, err
}
go func() { // server might not restart as often
threeDays := 5 * 24 * time.Hour
ticker := time.NewTicker(threeDays)
for range ticker.C {
err := transact(db, func(tx *sql.Tx) error {
return deleteConsumedInvites(tx)
})
if err != nil {
// TODO: hook up logging
log.Printf("admindb: failed to clean up old invites: %s", err.Error())
}
}
}()
al := &AllowList{db}
admindb := &Database{
db: db,
AuthWithSSB: AuthWithSSB{db},
AuthFallback: AuthFallback{db},
AllowList: AllowList{db},
AllowList: al,
Aliases: Aliases{db},
PinnedNotices: PinnedNotices{db},
Notices: Notices{db},
Invites: Invites{
db: db,
allowList: al,
},
}
return admindb, nil

View File

@ -22,8 +22,9 @@ func TestSimple(t *testing.T) {
require.NoError(t, err)
ctx := context.Background()
err = db.AuthFallback.Create(ctx, "testUser", []byte("super-cheesy-password-12345"))
uid, err := db.AuthFallback.Create(ctx, "testUser", []byte("super-cheesy-password-12345"))
require.NoError(t, err)
require.NotEqual(t, 0, uid)
err = db.Close()
require.NoError(t, err)

View File

@ -13,7 +13,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/admindb/sqlite/models"
)
// make sure to implement interfaces correctly
// compiler assertion to ensure the struct fullfills the interface
var _ admindb.PinnedNoticesService = (*PinnedNotices)(nil)
type PinnedNotices struct {
@ -112,7 +112,7 @@ func (pn PinnedNotices) Set(ctx context.Context, name admindb.PinnedNoticeName,
return nil
}
// make sure to implement interfaces correctly
// compiler assertion to ensure the struct fullfills the interface
var _ admindb.NoticesService = (*Notices)(nil)
type Notices struct {

View File

@ -17,7 +17,7 @@ import (
refs "go.mindeco.de/ssb-refs"
)
// make sure to implement interfaces correctly
// compiler assertion to ensure the struct fullfills the interface
var _ admindb.AllowListService = (*AllowList)(nil)
type AllowList struct {
@ -26,6 +26,14 @@ type AllowList struct {
// Add adds the feed to the list.
func (l AllowList) Add(ctx context.Context, a refs.FeedRef) error {
// single insert transaction but this makes it easier to re-use in invites.Consume
return transact(l.db, func(tx *sql.Tx) error {
return l.add(ctx, tx, a)
})
}
// this add is not exported and for internal use with transactions.
func (l AllowList) add(ctx context.Context, tx *sql.Tx, a refs.FeedRef) error {
// TODO: better valid
if _, err := refs.ParseFeedRef(a.Ref()); err != nil {
return err
@ -34,7 +42,7 @@ func (l AllowList) Add(ctx context.Context, a refs.FeedRef) error {
var entry models.AllowList
entry.PubKey.FeedRef = a
err := entry.Insert(ctx, l.db, boil.Whitelist("pub_key"))
err := entry.Insert(ctx, tx, boil.Whitelist("pub_key"))
if err != nil {
var sqlErr sqlite3.Error
if errors.As(err, &sqlErr) && sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique {

View File

@ -36,7 +36,10 @@ func TestAllowList(t *testing.T) {
err = db.AllowList.Add(ctx, okFeed)
r.NoError(err)
count, err := models.AllowLists().Count(ctx, db.AllowList.(AllowList).db)
// hack into the interface to get the concrete database/sql instance
sqlDB := db.AllowList.(*AllowList).db
count, err := models.AllowLists().Count(ctx, sqlDB)
r.NoError(err)
r.EqualValues(count, 1)
@ -53,7 +56,7 @@ func TestAllowList(t *testing.T) {
err = db.AllowList.RemoveFeed(ctx, okFeed)
r.NoError(err)
count, err = models.AllowLists().Count(ctx, db.AllowList.(AllowList).db)
count, err = models.AllowLists().Count(ctx, sqlDB)
r.NoError(err)
r.EqualValues(count, 0)

View File

@ -14,8 +14,6 @@ import (
// ErrNotFound is returned by the admin db if an object couldn't be found.
var ErrNotFound = errors.New("admindb: object not found")
// It's important to wrap all the model generated types into these since we don't want the admindb interfaces to depend on them.
// User holds all the information an authenticated user of the site has.
type User struct {
ID int64
@ -30,6 +28,16 @@ func (aa ErrAlreadyAdded) Error() string {
return fmt.Sprintf("admindb: the item (%s) is already on the list", aa.Ref.Ref())
}
// Invite is a combination of an invite id, who created it and an (optional) alias suggestion.
// The token itself is only visible from the db.Create function and stored hashed in the database
type Invite struct {
ID int64
CreatedBy User
AliasSuggestion string
}
// ListEntry values are returned by Allow- and DenyListServices
type ListEntry struct {
ID int64

View File

@ -9,12 +9,11 @@ import (
"os"
"syscall"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/ssh/terminal"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb/sqlite"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
)
func main() {
@ -44,9 +43,12 @@ func main() {
os.Exit(1)
return
}
ctx := context.Background()
err = db.AuthFallback.Create(ctx, os.Args[2], bytePassword)
uid, err := db.AuthFallback.Create(ctx, os.Args[2], bytePassword)
check(err)
fmt.Fprintln(os.Stderr, "created user with ID", uid)
}
func check(err error) {

1
go.mod
View File

@ -22,6 +22,7 @@ require (
github.com/stretchr/testify v1.6.1
github.com/unrolled/secure v1.0.8
github.com/vcraescu/go-paginator/v2 v2.0.0
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14 // indirect
github.com/volatiletech/sqlboiler/v4 v4.4.0
github.com/volatiletech/strmangle v0.0.1
go.cryptoscope.co/muxrpc/v2 v2.0.0-20210202162901-fe642d405dc6

16
go.sum
View File

@ -98,6 +98,7 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.6.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -195,6 +196,7 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@ -202,6 +204,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -248,6 +251,7 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -269,6 +273,7 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -307,6 +312,7 @@ github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736/go.mod h1:L3UMQOTh
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
@ -371,18 +377,23 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@ -394,6 +405,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -418,6 +430,9 @@ github.com/volatiletech/null/v8 v8.1.0 h1:eAO3I31A5R04usY5SKMMfDcOCnEGyT/T4wRI0J
github.com/volatiletech/null/v8 v8.1.0/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g=
github.com/volatiletech/randomize v0.0.1 h1:eE5yajattWqTB2/eN8df4dw+8jwAzBtbdo5sbWC4nMk=
github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY=
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14 h1:2PCMsnM/GVptZVyB8s0vTIFCPjl6f5rhz5Ry5MNShMQ=
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14/go.mod h1:fmZQG/eGdD2vdjWZjrVq4v2sTQ+Alz/I09chjYWWUVw=
github.com/volatiletech/sqlboiler/v4 v4.0.0/go.mod h1:U0Z5K4y+twWgHxh364G45QyzyNssSbBqNWtXGHVTlgM=
github.com/volatiletech/sqlboiler/v4 v4.4.0 h1:aSlvHidRBuxHHQZNX3ZLGgzNVPVPzWqsC3lhcLbV/b0=
github.com/volatiletech/sqlboiler/v4 v4.4.0/go.mod h1:h4RBAO6QbwMP3ezGmtfGljRms7S27cFIgF3rKgPKstE=
github.com/volatiletech/strmangle v0.0.1 h1:UKQoHmY6be/R3tSvD2nQYrH41k43OJkidwEiC74KIzk=
@ -587,6 +602,7 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=