From 4558b208ee2ce39379884450150208c638dca11b Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 11 May 2021 11:05:17 +0200 Subject: [PATCH] implement password update flow with reset tokens (fixes #98) also update AuthFallback database * re-write fallback auth to use alias or ssbid * replace Create() with SetPassword() which does an upsert * Add reset tokens to sqlite * add test for SetPassword with reset token --- .github/workflows/go.yml | 2 +- cmd/insert-user/main.go | 40 +- roomdb/interface.go | 14 +- roomdb/mockdb/auth_fallback.go | 266 +++- roomdb/sqlite/auth_fallback.go | 191 ++- roomdb/sqlite/auth_fallback_test.go | 159 +++ roomdb/sqlite/invites.go | 4 - roomdb/sqlite/migrations/01-consolidated.sql | 10 +- roomdb/sqlite/migrations/03-siwssb-tokens.sql | 4 +- .../migrations/04-overhaul-fallback-auth.sql | 56 + roomdb/sqlite/models/boil_table_names.go | 42 +- roomdb/sqlite/models/fallback_passwords.go | 19 +- roomdb/sqlite/models/fallback_reset_tokens.go | 1143 +++++++++++++++++ roomdb/sqlite/models/members.go | 442 ++++++- roomdb/sqlite/new.go | 12 +- roomdb/sqlite/new_test.go | 26 - web/handlers/admin/handler.go | 5 + web/handlers/admin/members.go | 36 +- web/handlers/http.go | 56 +- web/i18n/defaults/active.de.toml | 9 +- web/i18n/defaults/active.en.toml | 7 +- web/templates/admin/member.tmpl | 3 +- .../members-show-password-reset-token.tmpl | 19 + web/templates/change-member-password.tmpl | 15 +- 24 files changed, 2340 insertions(+), 240 deletions(-) create mode 100644 roomdb/sqlite/auth_fallback_test.go create mode 100644 roomdb/sqlite/migrations/04-overhaul-fallback-auth.sql create mode 100644 roomdb/sqlite/models/fallback_reset_tokens.go create mode 100644 web/templates/admin/members-show-password-reset-token.tmpl diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a6f31a7..a29fa8c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -27,7 +27,7 @@ jobs: run: go get -v -t -d ./... - name: Build smoke test - run: go build ./cmd/server + run: go build ./cmd/... - name: install node ssb-stack run: | diff --git a/cmd/insert-user/main.go b/cmd/insert-user/main.go index 6fe3062..2eff702 100644 --- a/cmd/insert-user/main.go +++ b/cmd/insert-user/main.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -// insert-user is a utility to create a new member and password +// insert-user is a utility to create a new member and fallback password for them package main import ( @@ -28,24 +28,10 @@ func main() { check(err) var ( - login string - pubKey *refs.FeedRef role roomdb.Role = roomdb.RoleAdmin repoPath string ) - flag.StringVar(&login, "login", "", "username (used when logging into the room's web ui)") - flag.Func("key", "the public key of the user, format: @.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") - } - key, err := refs.ParseFeedRef(val) - if err != nil { - return fmt.Errorf("%s\n", err) - } - pubKey = key - return nil - }) flag.StringVar(&repoPath, "repo", filepath.Join(u.HomeDir, ".ssb-go-room"), "[optional] where the locally stored files of the room is located") flag.Func("role", "[optional] which role the new member should have (values: mod[erator], admin, or member. default is admin)", func(val string) error { switch strings.ToLower(val) { @@ -65,18 +51,15 @@ func main() { }) flag.Parse() - /* we require at least 5 arguments: + -login + -key */ - /* 1 2 3 4 5 */ - if len(os.Args) < 5 { - cliMissingArguments("please provide the default arguments -login and -key") + // we require one more argument which is not a flag. + if len(flag.Args()) != 1 { + cliMissingArguments("please provide a public key") } - if login == "" { - cliMissingArguments("please provide a username with -login ") - } - - if pubKey == nil { - cliMissingArguments("please provide a public key with -key") + pubKey, err := refs.ParseFeedRef(flag.Arg(0)) + if err != nil { + fmt.Fprintln(os.Stderr, "Invalid ssb public-key referenfce:", err) + os.Exit(1) } r := repo.New(repoPath) @@ -95,22 +78,21 @@ func main() { if !bytes.Equal(bytePassword, bytePasswordRepeat) { fmt.Fprintln(os.Stderr, "Passwords didn't match") os.Exit(1) - return } ctx := context.Background() mid, err := db.Members.Add(ctx, *pubKey, role) check(err) - err = db.AuthFallback.Create(ctx, mid, login, bytePassword) + err = db.AuthFallback.SetPassword(ctx, mid, string(bytePassword)) check(err) - fmt.Fprintf(os.Stderr, "Created member %s (%s) with ID %d\n", login, role, mid) + fmt.Fprintf(os.Stderr, "Created member (%s) with ID %d\n", role, mid) } func cliMissingArguments(message string) { executable := strings.TrimPrefix(os.Args[0], "./") - fmt.Fprintf(os.Stderr, "%s: %s\nusage:%s -login -key <@.ed25519> \n", executable, message, executable) + fmt.Fprintf(os.Stderr, "%s: %s\nusage:%s <@base64-encoded-public-key=.ed25519> \n", executable, message, executable) flag.Usage() os.Exit(1) } diff --git a/roomdb/interface.go b/roomdb/interface.go index 1387d40..6e083ce 100644 --- a/roomdb/interface.go +++ b/roomdb/interface.go @@ -29,14 +29,18 @@ type RoomConfig interface { type AuthFallbackService interface { // Check receives the username and password (in clear) and checks them accordingly. + // Login might be a registered alias or a ssb id who belongs to a member. // If it's a valid combination it returns the user ID, or an error if they are not. auth.Auther - Create(_ context.Context, memberID int64, login string, password []byte) error - // GetByID(context.Context, int64) (User, error) - // ListAll() - // ListByMember() - // Remove(pwid) + // SetPassword creates or updates a fallback login password for this user. + SetPassword(_ context.Context, memberID int64, password []byte) error + + // CreateResetToken returns a token which can be used via SetPasswordWithToken() to reset the password of a member. + CreateResetToken(_ context.Context, createdByMember, forMember int64) (string, error) + + // SetPasswordWithToken consumes a token created with CreateResetToken() and updates the password for that member accordingly. + SetPasswordWithToken(_ context.Context, resetToken string, password []byte) error } // AuthWithSSBService defines utility functions for the challenge/response system of sign-in with ssb diff --git a/roomdb/mockdb/auth_fallback.go b/roomdb/mockdb/auth_fallback.go index 958da03..4e1ff6a 100644 --- a/roomdb/mockdb/auth_fallback.go +++ b/roomdb/mockdb/auth_fallback.go @@ -23,18 +23,45 @@ type FakeAuthFallbackService struct { result1 interface{} result2 error } - CreateStub func(context.Context, int64, string, []byte) error - createMutex sync.RWMutex - createArgsForCall []struct { + CreateResetTokenStub func(context.Context, int64, int64) (string, error) + createResetTokenMutex sync.RWMutex + createResetTokenArgsForCall []struct { arg1 context.Context arg2 int64 - arg3 string - arg4 []byte + arg3 int64 } - createReturns struct { + createResetTokenReturns struct { + result1 string + result2 error + } + createResetTokenReturnsOnCall map[int]struct { + result1 string + result2 error + } + SetPasswordStub func(context.Context, int64, []byte) error + setPasswordMutex sync.RWMutex + setPasswordArgsForCall []struct { + arg1 context.Context + arg2 int64 + arg3 []byte + } + setPasswordReturns struct { result1 error } - createReturnsOnCall map[int]struct { + setPasswordReturnsOnCall map[int]struct { + result1 error + } + SetPasswordWithTokenStub func(context.Context, string, []byte) error + setPasswordWithTokenMutex sync.RWMutex + setPasswordWithTokenArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 []byte + } + setPasswordWithTokenReturns struct { + result1 error + } + setPasswordWithTokenReturnsOnCall map[int]struct { result1 error } invocations map[string][][]interface{} @@ -106,26 +133,91 @@ func (fake *FakeAuthFallbackService) CheckReturnsOnCall(i int, result1 interface }{result1, result2} } -func (fake *FakeAuthFallbackService) Create(arg1 context.Context, arg2 int64, arg3 string, arg4 []byte) error { - var arg4Copy []byte - if arg4 != nil { - arg4Copy = make([]byte, len(arg4)) - copy(arg4Copy, arg4) - } - fake.createMutex.Lock() - ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)] - fake.createArgsForCall = append(fake.createArgsForCall, struct { +func (fake *FakeAuthFallbackService) CreateResetToken(arg1 context.Context, arg2 int64, arg3 int64) (string, error) { + fake.createResetTokenMutex.Lock() + ret, specificReturn := fake.createResetTokenReturnsOnCall[len(fake.createResetTokenArgsForCall)] + fake.createResetTokenArgsForCall = append(fake.createResetTokenArgsForCall, struct { arg1 context.Context arg2 int64 - arg3 string - arg4 []byte - }{arg1, arg2, arg3, arg4Copy}) - stub := fake.CreateStub - fakeReturns := fake.createReturns - fake.recordInvocation("Create", []interface{}{arg1, arg2, arg3, arg4Copy}) - fake.createMutex.Unlock() + arg3 int64 + }{arg1, arg2, arg3}) + stub := fake.CreateResetTokenStub + fakeReturns := fake.createResetTokenReturns + fake.recordInvocation("CreateResetToken", []interface{}{arg1, arg2, arg3}) + fake.createResetTokenMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4) + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeAuthFallbackService) CreateResetTokenCallCount() int { + fake.createResetTokenMutex.RLock() + defer fake.createResetTokenMutex.RUnlock() + return len(fake.createResetTokenArgsForCall) +} + +func (fake *FakeAuthFallbackService) CreateResetTokenCalls(stub func(context.Context, int64, int64) (string, error)) { + fake.createResetTokenMutex.Lock() + defer fake.createResetTokenMutex.Unlock() + fake.CreateResetTokenStub = stub +} + +func (fake *FakeAuthFallbackService) CreateResetTokenArgsForCall(i int) (context.Context, int64, int64) { + fake.createResetTokenMutex.RLock() + defer fake.createResetTokenMutex.RUnlock() + argsForCall := fake.createResetTokenArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeAuthFallbackService) CreateResetTokenReturns(result1 string, result2 error) { + fake.createResetTokenMutex.Lock() + defer fake.createResetTokenMutex.Unlock() + fake.CreateResetTokenStub = nil + fake.createResetTokenReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeAuthFallbackService) CreateResetTokenReturnsOnCall(i int, result1 string, result2 error) { + fake.createResetTokenMutex.Lock() + defer fake.createResetTokenMutex.Unlock() + fake.CreateResetTokenStub = nil + if fake.createResetTokenReturnsOnCall == nil { + fake.createResetTokenReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.createResetTokenReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeAuthFallbackService) SetPassword(arg1 context.Context, arg2 int64, arg3 []byte) error { + var arg3Copy []byte + if arg3 != nil { + arg3Copy = make([]byte, len(arg3)) + copy(arg3Copy, arg3) + } + fake.setPasswordMutex.Lock() + ret, specificReturn := fake.setPasswordReturnsOnCall[len(fake.setPasswordArgsForCall)] + fake.setPasswordArgsForCall = append(fake.setPasswordArgsForCall, struct { + arg1 context.Context + arg2 int64 + arg3 []byte + }{arg1, arg2, arg3Copy}) + stub := fake.SetPasswordStub + fakeReturns := fake.setPasswordReturns + fake.recordInvocation("SetPassword", []interface{}{arg1, arg2, arg3Copy}) + fake.setPasswordMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1 @@ -133,44 +225,112 @@ func (fake *FakeAuthFallbackService) Create(arg1 context.Context, arg2 int64, ar return fakeReturns.result1 } -func (fake *FakeAuthFallbackService) CreateCallCount() int { - fake.createMutex.RLock() - defer fake.createMutex.RUnlock() - return len(fake.createArgsForCall) +func (fake *FakeAuthFallbackService) SetPasswordCallCount() int { + fake.setPasswordMutex.RLock() + defer fake.setPasswordMutex.RUnlock() + return len(fake.setPasswordArgsForCall) } -func (fake *FakeAuthFallbackService) CreateCalls(stub func(context.Context, int64, string, []byte) error) { - fake.createMutex.Lock() - defer fake.createMutex.Unlock() - fake.CreateStub = stub +func (fake *FakeAuthFallbackService) SetPasswordCalls(stub func(context.Context, int64, []byte) error) { + fake.setPasswordMutex.Lock() + defer fake.setPasswordMutex.Unlock() + fake.SetPasswordStub = stub } -func (fake *FakeAuthFallbackService) CreateArgsForCall(i int) (context.Context, int64, string, []byte) { - fake.createMutex.RLock() - defer fake.createMutex.RUnlock() - argsForCall := fake.createArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +func (fake *FakeAuthFallbackService) SetPasswordArgsForCall(i int) (context.Context, int64, []byte) { + fake.setPasswordMutex.RLock() + defer fake.setPasswordMutex.RUnlock() + argsForCall := fake.setPasswordArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } -func (fake *FakeAuthFallbackService) CreateReturns(result1 error) { - fake.createMutex.Lock() - defer fake.createMutex.Unlock() - fake.CreateStub = nil - fake.createReturns = struct { +func (fake *FakeAuthFallbackService) SetPasswordReturns(result1 error) { + fake.setPasswordMutex.Lock() + defer fake.setPasswordMutex.Unlock() + fake.SetPasswordStub = nil + fake.setPasswordReturns = struct { result1 error }{result1} } -func (fake *FakeAuthFallbackService) CreateReturnsOnCall(i int, result1 error) { - fake.createMutex.Lock() - defer fake.createMutex.Unlock() - fake.CreateStub = nil - if fake.createReturnsOnCall == nil { - fake.createReturnsOnCall = make(map[int]struct { +func (fake *FakeAuthFallbackService) SetPasswordReturnsOnCall(i int, result1 error) { + fake.setPasswordMutex.Lock() + defer fake.setPasswordMutex.Unlock() + fake.SetPasswordStub = nil + if fake.setPasswordReturnsOnCall == nil { + fake.setPasswordReturnsOnCall = make(map[int]struct { result1 error }) } - fake.createReturnsOnCall[i] = struct { + fake.setPasswordReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeAuthFallbackService) SetPasswordWithToken(arg1 context.Context, arg2 string, arg3 []byte) error { + var arg3Copy []byte + if arg3 != nil { + arg3Copy = make([]byte, len(arg3)) + copy(arg3Copy, arg3) + } + fake.setPasswordWithTokenMutex.Lock() + ret, specificReturn := fake.setPasswordWithTokenReturnsOnCall[len(fake.setPasswordWithTokenArgsForCall)] + fake.setPasswordWithTokenArgsForCall = append(fake.setPasswordWithTokenArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 []byte + }{arg1, arg2, arg3Copy}) + stub := fake.SetPasswordWithTokenStub + fakeReturns := fake.setPasswordWithTokenReturns + fake.recordInvocation("SetPasswordWithToken", []interface{}{arg1, arg2, arg3Copy}) + fake.setPasswordWithTokenMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeAuthFallbackService) SetPasswordWithTokenCallCount() int { + fake.setPasswordWithTokenMutex.RLock() + defer fake.setPasswordWithTokenMutex.RUnlock() + return len(fake.setPasswordWithTokenArgsForCall) +} + +func (fake *FakeAuthFallbackService) SetPasswordWithTokenCalls(stub func(context.Context, string, []byte) error) { + fake.setPasswordWithTokenMutex.Lock() + defer fake.setPasswordWithTokenMutex.Unlock() + fake.SetPasswordWithTokenStub = stub +} + +func (fake *FakeAuthFallbackService) SetPasswordWithTokenArgsForCall(i int) (context.Context, string, []byte) { + fake.setPasswordWithTokenMutex.RLock() + defer fake.setPasswordWithTokenMutex.RUnlock() + argsForCall := fake.setPasswordWithTokenArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeAuthFallbackService) SetPasswordWithTokenReturns(result1 error) { + fake.setPasswordWithTokenMutex.Lock() + defer fake.setPasswordWithTokenMutex.Unlock() + fake.SetPasswordWithTokenStub = nil + fake.setPasswordWithTokenReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeAuthFallbackService) SetPasswordWithTokenReturnsOnCall(i int, result1 error) { + fake.setPasswordWithTokenMutex.Lock() + defer fake.setPasswordWithTokenMutex.Unlock() + fake.SetPasswordWithTokenStub = nil + if fake.setPasswordWithTokenReturnsOnCall == nil { + fake.setPasswordWithTokenReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setPasswordWithTokenReturnsOnCall[i] = struct { result1 error }{result1} } @@ -180,8 +340,12 @@ func (fake *FakeAuthFallbackService) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.checkMutex.RLock() defer fake.checkMutex.RUnlock() - fake.createMutex.RLock() - defer fake.createMutex.RUnlock() + fake.createResetTokenMutex.RLock() + defer fake.createResetTokenMutex.RUnlock() + fake.setPasswordMutex.RLock() + defer fake.setPasswordMutex.RUnlock() + fake.setPasswordWithTokenMutex.RLock() + defer fake.setPasswordWithTokenMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/roomdb/sqlite/auth_fallback.go b/roomdb/sqlite/auth_fallback.go index 53bd9d6..7a49558 100644 --- a/roomdb/sqlite/auth_fallback.go +++ b/roomdb/sqlite/auth_fallback.go @@ -4,10 +4,14 @@ 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" "go.mindeco.de/http/auth" @@ -30,15 +34,36 @@ var redirectPasswordAuthErr = weberrors.ErrRedirect{ Reason: auth.ErrBadLogin, } -// Check receives the username and password (in clear) and checks them accordingly. +// Check receives the loging and password (in clear) and checks them accordingly. +// Login might be a registered alias or a ssb id who belongs to a member. // If it's a valid combination it returns the user ID, or an error if they are not. func (af AuthFallback) Check(login, password string) (interface{}, error) { - ctx := context.Background() - found, err := models.FallbackPasswords( - qm.Load("Member"), - qm.Where("login = ?", login), - ).One(ctx, af.db) + + var memberID = int64(-1) + + // try looking up an alias first + loginIsAlias, err := models.Aliases(qm.Where("name = ?", login)).One(ctx, af.db) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + // something else went wrong + return nil, err + } + + // did not find an alias, try as ssb reference + member, err := models.Members(qm.Where("pub_key = ?", login)).One(ctx, af.db) + if errors.Is(err, sql.ErrNoRows) { + return nil, redirectPasswordAuthErr + } + + // member found by ssb id, use their id + memberID = member.ID + } else { + // found an alias, use the corresponding member ID + memberID = loginIsAlias.MemberID + } + + foundPassword, err := models.FallbackPasswords(qm.Where("member_id = ?", memberID)).One(ctx, af.db) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, redirectPasswordAuthErr @@ -46,29 +71,161 @@ func (af AuthFallback) Check(login, password string) (interface{}, error) { return nil, err } - err = bcrypt.CompareHashAndPassword(found.PasswordHash, []byte(password)) + err = bcrypt.CompareHashAndPassword(foundPassword.PasswordHash, []byte(password)) if err != nil { return nil, redirectPasswordAuthErr } - return found.R.Member.ID, nil + return foundPassword.MemberID, nil } -func (af AuthFallback) Create(ctx context.Context, memberID int64, login string, password []byte) error { - var newPasswordEntry models.FallbackPassword - newPasswordEntry.MemberID = memberID - newPasswordEntry.Login = login - +func (af AuthFallback) SetPassword(ctx context.Context, memberID int64, password []byte) error { hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) if err != nil { - return fmt.Errorf("auth/fallback: failed to hash password for new user") + return fmt.Errorf("auth/fallback: failed to hash password for member") } - newPasswordEntry.PasswordHash = hashed - err = newPasswordEntry.Insert(ctx, af.db, boil.Infer()) + // this is a silly upsert construction which sqlboiler-sqlite doesnt support nativly + return transact(af.db, func(tx *sql.Tx) error { + + foundPassword, err := models.FallbackPasswords(qm.Where("member_id = ?", memberID)).One(ctx, tx) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + // something else went wrong + return err + } + // not found => insert new entry + var newPasswordEntry models.FallbackPassword + newPasswordEntry.PasswordHash = hashed + newPasswordEntry.MemberID = memberID + err = newPasswordEntry.Insert(ctx, tx, boil.Infer()) + if err != nil { + return fmt.Errorf("auth/fallback: failed to insert new user: %w", err) + } + } else { + // found => update the entry + foundPassword.PasswordHash = hashed + _, err = foundPassword.Update(ctx, tx, boil.Whitelist("password_hash")) + if err != nil { + return fmt.Errorf("auth/fallback: failed to update password for member: %w", err) + } + } + + return nil + }) +} + +func (af AuthFallback) SetPasswordWithToken(ctx context.Context, resetToken string, password []byte) error { + hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) if err != nil { - return fmt.Errorf("auth/fallback: failed to insert new user: %w", err) + return fmt.Errorf("auth/fallback: failed to hash password for member") } + hashedTok, err := getHashedToken(resetToken) + if err != nil { + return fmt.Errorf("invalid password reset token") + } + + // this is a silly upsert construction which sqlboiler-sqlite doesnt support nativly + return transact(af.db, func(tx *sql.Tx) error { + // make sure its a valid one and load it + resetEntry, err := models.FallbackResetTokens(qm.Where("active = true and hashed_token = ?", hashedTok)).One(ctx, tx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("could not find the reset-token") + } + return err + } + + // see that there is a password entry for the member in the reset entry + foundPassword, err := models.FallbackPasswords(qm.Where("member_id = ?", resetEntry.ForMember)).One(ctx, tx) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return err + } + // not found => insert new entry + var newPasswordEntry models.FallbackPassword + newPasswordEntry.PasswordHash = hashed + newPasswordEntry.MemberID = resetEntry.ForMember + err = newPasswordEntry.Insert(ctx, tx, boil.Infer()) + if err != nil { + return fmt.Errorf("auth/fallback: failed to insert new fallback password for member: %w", err) + } + } else { + // found it => update the entry + foundPassword.PasswordHash = hashed + _, err = foundPassword.Update(ctx, tx, boil.Whitelist("password_hash")) + if err != nil { + return fmt.Errorf("auth/fallback: failed to update password for member: %w", err) + } + } + + // finally, invalidate the token + resetEntry.Active = false + _, err = resetEntry.Update(ctx, tx, boil.Whitelist("active")) + if err != nil { + return fmt.Errorf("auth/fallback: failed to invalidate the reset entry: %w", err) + } + + return nil + }) +} + +func (af AuthFallback) CreateResetToken(ctx context.Context, createdByMember, forMember int64) (string, error) { + var newResetToken = models.FallbackResetToken{ + CreatedBy: createdByMember, + ForMember: forMember, + } + + tokenBytes := make([]byte, inviteTokenLength) + + err := transact(af.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) + newResetToken.HashedToken = fmt.Sprintf("%x", h.Sum(nil)) + + // insert the new invite + err := newResetToken.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 +} + +// since reset tokens are marked as inavalid so that the code can't be generated twice, +// they need to be deleted periodically. +func deleteConsumedResetTokens(tx boil.ContextExecutor) error { + _, err := models.FallbackResetTokens(qm.Where("active = false")).DeleteAll(context.Background(), tx) + if err != nil { + return fmt.Errorf("admindb: failed to delete used reset tokens: %w", err) + } return nil } diff --git a/roomdb/sqlite/auth_fallback_test.go b/roomdb/sqlite/auth_fallback_test.go new file mode 100644 index 0000000..cc2f975 --- /dev/null +++ b/roomdb/sqlite/auth_fallback_test.go @@ -0,0 +1,159 @@ +package sqlite + +import ( + "bytes" + "context" + "crypto/rand" + "os" + "path/filepath" + "testing" + + "github.com/ssb-ngi-pointer/go-ssb-room/internal/repo" + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + "github.com/stretchr/testify/require" + refs "go.mindeco.de/ssb-refs" +) + +func TestFallbackAuth(t *testing.T) { + r := require.New(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) + r.NoError(err, "failed to open database") + + memberID, err := db.Members.Add(ctx, newMember, roomdb.RoleMember) + r.NoError(err, "failed to create member") + + testPassword := []byte("super-secure-and-secret-password") + + err = db.AuthFallback.SetPassword(ctx, memberID, testPassword) + r.NoError(err, "failed to create password") + + cookieVal, err := db.AuthFallback.Check(newMember.Ref(), string(testPassword)) + r.NoError(err, "failed to check password") + gotID, ok := cookieVal.(int64) + r.True(ok, "unexpected cookie value: %T", cookieVal) + r.Equal(memberID, gotID, "unexpected member ID value") + + // now check we can also use an alias + testAliasLogin := "test-alias-login" + + // 64 bytes of random for testing (validation is handled by the handlers) + testSig := make([]byte, 64) + rand.Read(testSig) + + err = db.Aliases.Register(ctx, testAliasLogin, newMember, testSig) + r.NoError(err, "failed to register the test alias") + + cookieVal2, err := db.AuthFallback.Check(testAliasLogin, string(testPassword)) + r.NoError(err, "failed to check password via alias") + gotIDforAlias, ok := cookieVal2.(int64) + r.True(ok, "unexpected cookie value: %T", cookieVal) + r.Equal(memberID, gotIDforAlias, "unexpected member ID value") + + r.NoError(db.Close()) +} + +func TestFallbackAuthSetPassword(t *testing.T) { + r := require.New(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) + r.NoError(err, "failed to open database") + + memberID, err := db.Members.Add(ctx, newMember, roomdb.RoleMember) + r.NoError(err, "failed to create member") + + testPassword := []byte("super-secure-and-secret-password") + + err = db.AuthFallback.SetPassword(ctx, memberID, testPassword) + r.NoError(err, "failed to set password") + + // use the password + cookieVal, err := db.AuthFallback.Check(newMember.Ref(), string(testPassword)) + r.NoError(err, "failed to check password") + gotID, ok := cookieVal.(int64) + r.True(ok, "unexpected cookie value: %T", cookieVal) + r.Equal(memberID, gotID, "unexpected member ID value") + + // use a wrong password + cookieVal, err = db.AuthFallback.Check(newMember.Ref(), string(testPassword)+"nope-nope-nope") + r.Error(err, "wrong password actually worked?!") + r.Nil(cookieVal) + + // set it to something different + changedTestPassword := []byte("some-different-super-secure-password") + err = db.AuthFallback.SetPassword(ctx, memberID, changedTestPassword) + r.NoError(err, "failed to update password") + + // now try to use old and new + cookieVal, err = db.AuthFallback.Check(newMember.Ref(), string(testPassword)) + r.Error(err, "old password actually worked?!") + r.Nil(cookieVal) + + cookieVal, err = db.AuthFallback.Check(newMember.Ref(), string(changedTestPassword)) + r.NoError(err, "new password didnt work") + gotID, ok = cookieVal.(int64) + r.True(ok, "unexpected cookie value: %T", cookieVal) + r.Equal(memberID, gotID, "unexpected member ID value") +} + +func TestFallbackAuthSetPasswordWithToken(t *testing.T) { + r := require.New(t) + ctx := context.Background() + + testRepo := filepath.Join("testrun", t.Name()) + os.RemoveAll(testRepo) + tr := repo.New(testRepo) + + // two fake feeds for testing, looks ok at least + alf := refs.FeedRef{ID: bytes.Repeat([]byte("whyy"), 8), Algo: refs.RefAlgoFeedSSB1} + carl := refs.FeedRef{ID: bytes.Repeat([]byte("carl"), 8), Algo: refs.RefAlgoFeedSSB1} + + db, err := Open(tr) + r.NoError(err, "failed to open database") + + alfID, err := db.Members.Add(ctx, alf, roomdb.RoleModerator) + r.NoError(err, "failed to create member") + + carlID, err := db.Members.Add(ctx, carl, roomdb.RoleModerator) + r.NoError(err, "failed to create member") + + err = db.AuthFallback.SetPassword(ctx, carlID, []byte("i swear i wont forgettt thiszzz91238129e812hjejahsdkasdhaksjdh")) + r.NoError(err, "failed to update password") + + // and he does... so lets create a token for him + resetTok, err := db.AuthFallback.CreateResetToken(ctx, alfID, carlID) + r.NoError(err) + + // has to be a from valid user tho + noToken, err := db.AuthFallback.CreateResetToken(ctx, 666, carlID) + r.Error(err) + r.Equal("", noToken) + + // change carls password by using the token + newPassword := "marry had a little lamp" + err = db.AuthFallback.SetPasswordWithToken(ctx, resetTok, []byte(newPassword)) + r.NoError(err, "setPassword with token failed") + + // now use the new password + cookieVal, err := db.AuthFallback.Check(carl.Ref(), newPassword) + r.NoError(err, "new password didnt work") + gotID, ok := cookieVal.(int64) + r.True(ok, "unexpected cookie value: %T", cookieVal) + r.Equal(carlID, gotID, "unexpected member ID value") +} diff --git a/roomdb/sqlite/invites.go b/roomdb/sqlite/invites.go index f57f19c..f50c89e 100644 --- a/roomdb/sqlite/invites.go +++ b/roomdb/sqlite/invites.go @@ -7,7 +7,6 @@ import ( "database/sql" "encoding/base64" "fmt" - "time" "github.com/friendsofgo/errors" "github.com/mattn/go-sqlite3" @@ -49,9 +48,6 @@ func (i Invites) Create(ctx context.Context, createdBy int64) (string, error) { // generate an invite code rand.Read(tokenBytes) - // see comment on migrations/6-invite-createdAt.sql - newInvite.CreatedAt = time.Now() - // hash the binary of the token for storage h := sha256.New() h.Write(tokenBytes) diff --git a/roomdb/sqlite/migrations/01-consolidated.sql b/roomdb/sqlite/migrations/01-consolidated.sql index 1e3cdbf..0c82d9f 100644 --- a/roomdb/sqlite/migrations/01-consolidated.sql +++ b/roomdb/sqlite/migrations/01-consolidated.sql @@ -60,17 +60,17 @@ CREATE INDEX denied_keys_by_pubkey ON invites(active); -- +migrate Down DROP TABLE members; -DROP TABLE fallback_passwords; DROP INDEX fallback_passwords_by_login; +DROP TABLE fallback_passwords; -DROP TABLE invites; DROP INDEX invite_active_ids; DROP INDEX invite_active_tokens; DROP INDEX invite_inactive; +DROP TABLE invites; -DROP TABLE aliases; DROP INDEX aliases_ids; DROP INDEX aliases_names; +DROP TABLE aliases; -DROP TABLE denied_keys; -DROP INDEX denied_keys_by_pubkey; \ No newline at end of file +DROP INDEX denied_keys_by_pubkey; +DROP TABLE denied_keys; \ No newline at end of file diff --git a/roomdb/sqlite/migrations/03-siwssb-tokens.sql b/roomdb/sqlite/migrations/03-siwssb-tokens.sql index 834846d..4cc19f3 100644 --- a/roomdb/sqlite/migrations/03-siwssb-tokens.sql +++ b/roomdb/sqlite/migrations/03-siwssb-tokens.sql @@ -12,6 +12,6 @@ CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token); CREATE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id); -- +migrate Down -DROP TABLE SIWSSB_sessions; DROP INDEX SIWSSB_by_token; -DROP INDEX SIWSSB_by_member; \ No newline at end of file +DROP INDEX SIWSSB_by_member; +DROP TABLE SIWSSB_sessions; \ No newline at end of file diff --git a/roomdb/sqlite/migrations/04-overhaul-fallback-auth.sql b/roomdb/sqlite/migrations/04-overhaul-fallback-auth.sql new file mode 100644 index 0000000..13c5a01 --- /dev/null +++ b/roomdb/sqlite/migrations/04-overhaul-fallback-auth.sql @@ -0,0 +1,56 @@ +-- +migrate Up + +-- drop login column from fallback pw +-- ================================== + +-- this is sqlite style ALTER TABLE abc DROP COLUMN +-- See 5) in https://www.sqlite.org/lang_altertable.html +-- and https://www.sqlitetutorial.net/sqlite-alter-table/ + +-- drop obsolete index +DROP INDEX fallback_passwords_by_login; + +-- create new schema table (without 'login' column) +CREATE TABLE updated_passwords_table ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + password_hash BLOB NOT NULL, + + member_id INTEGER UNIQUE NOT NULL, + + FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE +); + +-- copy existing values from original table into new +INSERT INTO updated_passwords_table(id, password_hash, member_id) +SELECT id, password_hash, member_id +FROM fallback_passwords; + +-- rename the new to the original table name +DROP TABLE fallback_passwords; +ALTER TABLE updated_passwords_table RENAME TO fallback_passwords; + +-- create new lookup index by member id +CREATE INDEX fallback_passwords_by_member ON fallback_passwords(member_id); + +-- add new table for password reset tokens +--======================================== +CREATE TABLE fallback_reset_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + hashed_token TEXT UNIQUE NOT NULL, + created_by INTEGER NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + for_member INTEGER NOT NULL, + + active boolean NOT NULL DEFAULT TRUE, + + FOREIGN KEY ( created_by ) REFERENCES members( "id" ) ON DELETE CASCADE + FOREIGN KEY ( for_member ) REFERENCES members( "id" ) ON DELETE CASCADE +); +CREATE INDEX fallback_reset_tokens_by_token ON fallback_reset_tokens(hashed_token); + +-- +migrate Down +DROP INDEX fallback_passwords_by_member; +DROP TABLE fallback_passwords; + +DROP TABLE fallback_reset_tokens; \ No newline at end of file diff --git a/roomdb/sqlite/models/boil_table_names.go b/roomdb/sqlite/models/boil_table_names.go index ece3a54..5f855b0 100644 --- a/roomdb/sqlite/models/boil_table_names.go +++ b/roomdb/sqlite/models/boil_table_names.go @@ -4,25 +4,27 @@ package models var TableNames = struct { - SIWSSBSessions string - Aliases string - Config string - DeniedKeys string - FallbackPasswords string - Invites string - Members string - Notices string - PinNotices string - Pins string + SIWSSBSessions string + Aliases string + Config string + DeniedKeys string + FallbackPasswords string + FallbackResetTokens string + Invites string + Members string + Notices string + PinNotices string + Pins string }{ - SIWSSBSessions: "SIWSSB_sessions", - Aliases: "aliases", - Config: "config", - DeniedKeys: "denied_keys", - FallbackPasswords: "fallback_passwords", - Invites: "invites", - Members: "members", - Notices: "notices", - PinNotices: "pin_notices", - Pins: "pins", + SIWSSBSessions: "SIWSSB_sessions", + Aliases: "aliases", + Config: "config", + DeniedKeys: "denied_keys", + FallbackPasswords: "fallback_passwords", + FallbackResetTokens: "fallback_reset_tokens", + Invites: "invites", + Members: "members", + Notices: "notices", + PinNotices: "pin_notices", + Pins: "pins", } diff --git a/roomdb/sqlite/models/fallback_passwords.go b/roomdb/sqlite/models/fallback_passwords.go index d112ffe..f682257 100644 --- a/roomdb/sqlite/models/fallback_passwords.go +++ b/roomdb/sqlite/models/fallback_passwords.go @@ -23,7 +23,6 @@ import ( // FallbackPassword is an object representing the database table. type FallbackPassword struct { ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"` - Login string `boil:"login" json:"login" toml:"login" yaml:"login"` PasswordHash []byte `boil:"password_hash" json:"password_hash" toml:"password_hash" yaml:"password_hash"` MemberID int64 `boil:"member_id" json:"member_id" toml:"member_id" yaml:"member_id"` @@ -33,12 +32,10 @@ type FallbackPassword struct { var FallbackPasswordColumns = struct { ID string - Login string PasswordHash string MemberID string }{ ID: "id", - Login: "login", PasswordHash: "password_hash", MemberID: "member_id", } @@ -47,12 +44,10 @@ var FallbackPasswordColumns = struct { var FallbackPasswordWhere = struct { ID whereHelperint64 - Login whereHelperstring PasswordHash whereHelper__byte MemberID whereHelperint64 }{ ID: whereHelperint64{field: "\"fallback_passwords\".\"id\""}, - Login: whereHelperstring{field: "\"fallback_passwords\".\"login\""}, PasswordHash: whereHelper__byte{field: "\"fallback_passwords\".\"password_hash\""}, MemberID: whereHelperint64{field: "\"fallback_passwords\".\"member_id\""}, } @@ -78,9 +73,9 @@ func (*fallbackPasswordR) NewStruct() *fallbackPasswordR { type fallbackPasswordL struct{} var ( - fallbackPasswordAllColumns = []string{"id", "login", "password_hash", "member_id"} + fallbackPasswordAllColumns = []string{"id", "password_hash", "member_id"} fallbackPasswordColumnsWithoutDefault = []string{} - fallbackPasswordColumnsWithDefault = []string{"id", "login", "password_hash", "member_id"} + fallbackPasswordColumnsWithDefault = []string{"id", "password_hash", "member_id"} fallbackPasswordPrimaryKeyColumns = []string{"id"} ) @@ -457,7 +452,7 @@ func (fallbackPasswordL) LoadMember(ctx context.Context, e boil.ContextExecutor, if foreign.R == nil { foreign.R = &memberR{} } - foreign.R.FallbackPasswords = append(foreign.R.FallbackPasswords, object) + foreign.R.FallbackPassword = object return nil } @@ -468,7 +463,7 @@ func (fallbackPasswordL) LoadMember(ctx context.Context, e boil.ContextExecutor, if foreign.R == nil { foreign.R = &memberR{} } - foreign.R.FallbackPasswords = append(foreign.R.FallbackPasswords, local) + foreign.R.FallbackPassword = local break } } @@ -479,7 +474,7 @@ func (fallbackPasswordL) LoadMember(ctx context.Context, e boil.ContextExecutor, // SetMember of the fallbackPassword to the related item. // Sets o.R.Member to related. -// Adds o to related.R.FallbackPasswords. +// Adds o to related.R.FallbackPassword. func (o *FallbackPassword) SetMember(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Member) error { var err error if insert { @@ -515,10 +510,10 @@ func (o *FallbackPassword) SetMember(ctx context.Context, exec boil.ContextExecu if related.R == nil { related.R = &memberR{ - FallbackPasswords: FallbackPasswordSlice{o}, + FallbackPassword: o, } } else { - related.R.FallbackPasswords = append(related.R.FallbackPasswords, o) + related.R.FallbackPassword = o } return nil diff --git a/roomdb/sqlite/models/fallback_reset_tokens.go b/roomdb/sqlite/models/fallback_reset_tokens.go new file mode 100644 index 0000000..a7a54c1 --- /dev/null +++ b/roomdb/sqlite/models/fallback_reset_tokens.go @@ -0,0 +1,1143 @@ +// Code generated by SQLBoiler 4.5.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" +) + +// FallbackResetToken is an object representing the database table. +type FallbackResetToken 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"` + ForMember int64 `boil:"for_member" json:"for_member" toml:"for_member" yaml:"for_member"` + Active bool `boil:"active" json:"active" toml:"active" yaml:"active"` + + R *fallbackResetTokenR `boil:"-" json:"-" toml:"-" yaml:"-"` + L fallbackResetTokenL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var FallbackResetTokenColumns = struct { + ID string + HashedToken string + CreatedBy string + CreatedAt string + ForMember string + Active string +}{ + ID: "id", + HashedToken: "hashed_token", + CreatedBy: "created_by", + CreatedAt: "created_at", + ForMember: "for_member", + Active: "active", +} + +// Generated where + +var FallbackResetTokenWhere = struct { + ID whereHelperint64 + HashedToken whereHelperstring + CreatedBy whereHelperint64 + CreatedAt whereHelpertime_Time + ForMember whereHelperint64 + Active whereHelperbool +}{ + ID: whereHelperint64{field: "\"fallback_reset_tokens\".\"id\""}, + HashedToken: whereHelperstring{field: "\"fallback_reset_tokens\".\"hashed_token\""}, + CreatedBy: whereHelperint64{field: "\"fallback_reset_tokens\".\"created_by\""}, + CreatedAt: whereHelpertime_Time{field: "\"fallback_reset_tokens\".\"created_at\""}, + ForMember: whereHelperint64{field: "\"fallback_reset_tokens\".\"for_member\""}, + Active: whereHelperbool{field: "\"fallback_reset_tokens\".\"active\""}, +} + +// FallbackResetTokenRels is where relationship names are stored. +var FallbackResetTokenRels = struct { + ForMemberMember string + CreatedByMember string +}{ + ForMemberMember: "ForMemberMember", + CreatedByMember: "CreatedByMember", +} + +// fallbackResetTokenR is where relationships are stored. +type fallbackResetTokenR struct { + ForMemberMember *Member `boil:"ForMemberMember" json:"ForMemberMember" toml:"ForMemberMember" yaml:"ForMemberMember"` + CreatedByMember *Member `boil:"CreatedByMember" json:"CreatedByMember" toml:"CreatedByMember" yaml:"CreatedByMember"` +} + +// NewStruct creates a new relationship struct +func (*fallbackResetTokenR) NewStruct() *fallbackResetTokenR { + return &fallbackResetTokenR{} +} + +// fallbackResetTokenL is where Load methods for each relationship are stored. +type fallbackResetTokenL struct{} + +var ( + fallbackResetTokenAllColumns = []string{"id", "hashed_token", "created_by", "created_at", "for_member", "active"} + fallbackResetTokenColumnsWithoutDefault = []string{} + fallbackResetTokenColumnsWithDefault = []string{"id", "hashed_token", "created_by", "created_at", "for_member", "active"} + fallbackResetTokenPrimaryKeyColumns = []string{"id"} +) + +type ( + // FallbackResetTokenSlice is an alias for a slice of pointers to FallbackResetToken. + // This should generally be used opposed to []FallbackResetToken. + FallbackResetTokenSlice []*FallbackResetToken + // FallbackResetTokenHook is the signature for custom FallbackResetToken hook methods + FallbackResetTokenHook func(context.Context, boil.ContextExecutor, *FallbackResetToken) error + + fallbackResetTokenQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + fallbackResetTokenType = reflect.TypeOf(&FallbackResetToken{}) + fallbackResetTokenMapping = queries.MakeStructMapping(fallbackResetTokenType) + fallbackResetTokenPrimaryKeyMapping, _ = queries.BindMapping(fallbackResetTokenType, fallbackResetTokenMapping, fallbackResetTokenPrimaryKeyColumns) + fallbackResetTokenInsertCacheMut sync.RWMutex + fallbackResetTokenInsertCache = make(map[string]insertCache) + fallbackResetTokenUpdateCacheMut sync.RWMutex + fallbackResetTokenUpdateCache = make(map[string]updateCache) + fallbackResetTokenUpsertCacheMut sync.RWMutex + fallbackResetTokenUpsertCache = 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 fallbackResetTokenBeforeInsertHooks []FallbackResetTokenHook +var fallbackResetTokenBeforeUpdateHooks []FallbackResetTokenHook +var fallbackResetTokenBeforeDeleteHooks []FallbackResetTokenHook +var fallbackResetTokenBeforeUpsertHooks []FallbackResetTokenHook + +var fallbackResetTokenAfterInsertHooks []FallbackResetTokenHook +var fallbackResetTokenAfterSelectHooks []FallbackResetTokenHook +var fallbackResetTokenAfterUpdateHooks []FallbackResetTokenHook +var fallbackResetTokenAfterDeleteHooks []FallbackResetTokenHook +var fallbackResetTokenAfterUpsertHooks []FallbackResetTokenHook + +// doBeforeInsertHooks executes all "before insert" hooks. +func (o *FallbackResetToken) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenBeforeInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpdateHooks executes all "before Update" hooks. +func (o *FallbackResetToken) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenBeforeUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeDeleteHooks executes all "before Delete" hooks. +func (o *FallbackResetToken) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenBeforeDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpsertHooks executes all "before Upsert" hooks. +func (o *FallbackResetToken) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenBeforeUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterInsertHooks executes all "after Insert" hooks. +func (o *FallbackResetToken) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenAfterInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterSelectHooks executes all "after Select" hooks. +func (o *FallbackResetToken) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenAfterSelectHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpdateHooks executes all "after Update" hooks. +func (o *FallbackResetToken) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenAfterUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterDeleteHooks executes all "after Delete" hooks. +func (o *FallbackResetToken) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenAfterDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpsertHooks executes all "after Upsert" hooks. +func (o *FallbackResetToken) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range fallbackResetTokenAfterUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// AddFallbackResetTokenHook registers your hook function for all future operations. +func AddFallbackResetTokenHook(hookPoint boil.HookPoint, fallbackResetTokenHook FallbackResetTokenHook) { + switch hookPoint { + case boil.BeforeInsertHook: + fallbackResetTokenBeforeInsertHooks = append(fallbackResetTokenBeforeInsertHooks, fallbackResetTokenHook) + case boil.BeforeUpdateHook: + fallbackResetTokenBeforeUpdateHooks = append(fallbackResetTokenBeforeUpdateHooks, fallbackResetTokenHook) + case boil.BeforeDeleteHook: + fallbackResetTokenBeforeDeleteHooks = append(fallbackResetTokenBeforeDeleteHooks, fallbackResetTokenHook) + case boil.BeforeUpsertHook: + fallbackResetTokenBeforeUpsertHooks = append(fallbackResetTokenBeforeUpsertHooks, fallbackResetTokenHook) + case boil.AfterInsertHook: + fallbackResetTokenAfterInsertHooks = append(fallbackResetTokenAfterInsertHooks, fallbackResetTokenHook) + case boil.AfterSelectHook: + fallbackResetTokenAfterSelectHooks = append(fallbackResetTokenAfterSelectHooks, fallbackResetTokenHook) + case boil.AfterUpdateHook: + fallbackResetTokenAfterUpdateHooks = append(fallbackResetTokenAfterUpdateHooks, fallbackResetTokenHook) + case boil.AfterDeleteHook: + fallbackResetTokenAfterDeleteHooks = append(fallbackResetTokenAfterDeleteHooks, fallbackResetTokenHook) + case boil.AfterUpsertHook: + fallbackResetTokenAfterUpsertHooks = append(fallbackResetTokenAfterUpsertHooks, fallbackResetTokenHook) + } +} + +// One returns a single fallbackResetToken record from the query. +func (q fallbackResetTokenQuery) One(ctx context.Context, exec boil.ContextExecutor) (*FallbackResetToken, error) { + o := &FallbackResetToken{} + + 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 fallback_reset_tokens") + } + + if err := o.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + + return o, nil +} + +// All returns all FallbackResetToken records from the query. +func (q fallbackResetTokenQuery) All(ctx context.Context, exec boil.ContextExecutor) (FallbackResetTokenSlice, error) { + var o []*FallbackResetToken + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "models: failed to assign all query results to FallbackResetToken slice") + } + + if len(fallbackResetTokenAfterSelectHooks) != 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 FallbackResetToken records in the query. +func (q fallbackResetTokenQuery) 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 fallback_reset_tokens rows") + } + + return count, nil +} + +// Exists checks if the row exists in the table. +func (q fallbackResetTokenQuery) 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 fallback_reset_tokens exists") + } + + return count > 0, nil +} + +// ForMemberMember pointed to by the foreign key. +func (o *FallbackResetToken) ForMemberMember(mods ...qm.QueryMod) memberQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"id\" = ?", o.ForMember), + } + + queryMods = append(queryMods, mods...) + + query := Members(queryMods...) + queries.SetFrom(query.Query, "\"members\"") + + return query +} + +// CreatedByMember pointed to by the foreign key. +func (o *FallbackResetToken) CreatedByMember(mods ...qm.QueryMod) memberQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"id\" = ?", o.CreatedBy), + } + + queryMods = append(queryMods, mods...) + + query := Members(queryMods...) + queries.SetFrom(query.Query, "\"members\"") + + return query +} + +// LoadForMemberMember allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for an N-1 relationship. +func (fallbackResetTokenL) LoadForMemberMember(ctx context.Context, e boil.ContextExecutor, singular bool, maybeFallbackResetToken interface{}, mods queries.Applicator) error { + var slice []*FallbackResetToken + var object *FallbackResetToken + + if singular { + object = maybeFallbackResetToken.(*FallbackResetToken) + } else { + slice = *maybeFallbackResetToken.(*[]*FallbackResetToken) + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &fallbackResetTokenR{} + } + args = append(args, object.ForMember) + + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &fallbackResetTokenR{} + } + + for _, a := range args { + if a == obj.ForMember { + continue Outer + } + } + + args = append(args, obj.ForMember) + + } + } + + if len(args) == 0 { + return nil + } + + query := NewQuery( + qm.From(`members`), + qm.WhereIn(`members.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 Member") + } + + var resultSlice []*Member + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice Member") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for members") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for members") + } + + if len(fallbackResetTokenAfterSelectHooks) != 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.ForMemberMember = foreign + if foreign.R == nil { + foreign.R = &memberR{} + } + foreign.R.ForMemberFallbackResetTokens = append(foreign.R.ForMemberFallbackResetTokens, object) + return nil + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if local.ForMember == foreign.ID { + local.R.ForMemberMember = foreign + if foreign.R == nil { + foreign.R = &memberR{} + } + foreign.R.ForMemberFallbackResetTokens = append(foreign.R.ForMemberFallbackResetTokens, local) + break + } + } + } + + return nil +} + +// LoadCreatedByMember allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for an N-1 relationship. +func (fallbackResetTokenL) LoadCreatedByMember(ctx context.Context, e boil.ContextExecutor, singular bool, maybeFallbackResetToken interface{}, mods queries.Applicator) error { + var slice []*FallbackResetToken + var object *FallbackResetToken + + if singular { + object = maybeFallbackResetToken.(*FallbackResetToken) + } else { + slice = *maybeFallbackResetToken.(*[]*FallbackResetToken) + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &fallbackResetTokenR{} + } + args = append(args, object.CreatedBy) + + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &fallbackResetTokenR{} + } + + 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(`members`), + qm.WhereIn(`members.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 Member") + } + + var resultSlice []*Member + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice Member") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for members") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for members") + } + + if len(fallbackResetTokenAfterSelectHooks) != 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.CreatedByMember = foreign + if foreign.R == nil { + foreign.R = &memberR{} + } + foreign.R.CreatedByFallbackResetTokens = append(foreign.R.CreatedByFallbackResetTokens, object) + return nil + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if local.CreatedBy == foreign.ID { + local.R.CreatedByMember = foreign + if foreign.R == nil { + foreign.R = &memberR{} + } + foreign.R.CreatedByFallbackResetTokens = append(foreign.R.CreatedByFallbackResetTokens, local) + break + } + } + } + + return nil +} + +// SetForMemberMember of the fallbackResetToken to the related item. +// Sets o.R.ForMemberMember to related. +// Adds o to related.R.ForMemberFallbackResetTokens. +func (o *FallbackResetToken) SetForMemberMember(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Member) 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 \"fallback_reset_tokens\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, []string{"for_member"}), + strmangle.WhereClause("\"", "\"", 0, fallbackResetTokenPrimaryKeyColumns), + ) + 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.ForMember = related.ID + if o.R == nil { + o.R = &fallbackResetTokenR{ + ForMemberMember: related, + } + } else { + o.R.ForMemberMember = related + } + + if related.R == nil { + related.R = &memberR{ + ForMemberFallbackResetTokens: FallbackResetTokenSlice{o}, + } + } else { + related.R.ForMemberFallbackResetTokens = append(related.R.ForMemberFallbackResetTokens, o) + } + + return nil +} + +// SetCreatedByMember of the fallbackResetToken to the related item. +// Sets o.R.CreatedByMember to related. +// Adds o to related.R.CreatedByFallbackResetTokens. +func (o *FallbackResetToken) SetCreatedByMember(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Member) 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 \"fallback_reset_tokens\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, []string{"created_by"}), + strmangle.WhereClause("\"", "\"", 0, fallbackResetTokenPrimaryKeyColumns), + ) + 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 = &fallbackResetTokenR{ + CreatedByMember: related, + } + } else { + o.R.CreatedByMember = related + } + + if related.R == nil { + related.R = &memberR{ + CreatedByFallbackResetTokens: FallbackResetTokenSlice{o}, + } + } else { + related.R.CreatedByFallbackResetTokens = append(related.R.CreatedByFallbackResetTokens, o) + } + + return nil +} + +// FallbackResetTokens retrieves all the records using an executor. +func FallbackResetTokens(mods ...qm.QueryMod) fallbackResetTokenQuery { + mods = append(mods, qm.From("\"fallback_reset_tokens\"")) + return fallbackResetTokenQuery{NewQuery(mods...)} +} + +// FindFallbackResetToken retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindFallbackResetToken(ctx context.Context, exec boil.ContextExecutor, iD int64, selectCols ...string) (*FallbackResetToken, error) { + fallbackResetTokenObj := &FallbackResetToken{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"fallback_reset_tokens\" where \"id\"=?", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, fallbackResetTokenObj) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "models: unable to select from fallback_reset_tokens") + } + + return fallbackResetTokenObj, nil +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *FallbackResetToken) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("models: no fallback_reset_tokens provided for insertion") + } + + var err error + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + } + + if err := o.doBeforeInsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(fallbackResetTokenColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + fallbackResetTokenInsertCacheMut.RLock() + cache, cached := fallbackResetTokenInsertCache[key] + fallbackResetTokenInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + fallbackResetTokenAllColumns, + fallbackResetTokenColumnsWithDefault, + fallbackResetTokenColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(fallbackResetTokenType, fallbackResetTokenMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(fallbackResetTokenType, fallbackResetTokenMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"fallback_reset_tokens\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"fallback_reset_tokens\" %sDEFAULT VALUES%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + cache.retQuery = fmt.Sprintf("SELECT \"%s\" FROM \"fallback_reset_tokens\" WHERE %s", strings.Join(returnColumns, "\",\""), strmangle.WhereClause("\"", "\"", 0, fallbackResetTokenPrimaryKeyColumns)) + } + + 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 fallback_reset_tokens") + } + + 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] == fallbackResetTokenMapping["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 fallback_reset_tokens") + } + +CacheNoHooks: + if !cached { + fallbackResetTokenInsertCacheMut.Lock() + fallbackResetTokenInsertCache[key] = cache + fallbackResetTokenInsertCacheMut.Unlock() + } + + return o.doAfterInsertHooks(ctx, exec) +} + +// Update uses an executor to update the FallbackResetToken. +// 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 *FallbackResetToken) 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) + fallbackResetTokenUpdateCacheMut.RLock() + cache, cached := fallbackResetTokenUpdateCache[key] + fallbackResetTokenUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + fallbackResetTokenAllColumns, + fallbackResetTokenPrimaryKeyColumns, + ) + + if !columns.IsWhitelist() { + wl = strmangle.SetComplement(wl, []string{"created_at"}) + } + if len(wl) == 0 { + return 0, errors.New("models: unable to update fallback_reset_tokens, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"fallback_reset_tokens\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, wl), + strmangle.WhereClause("\"", "\"", 0, fallbackResetTokenPrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(fallbackResetTokenType, fallbackResetTokenMapping, append(wl, fallbackResetTokenPrimaryKeyColumns...)) + 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 fallback_reset_tokens row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by update for fallback_reset_tokens") + } + + if !cached { + fallbackResetTokenUpdateCacheMut.Lock() + fallbackResetTokenUpdateCache[key] = cache + fallbackResetTokenUpdateCacheMut.Unlock() + } + + return rowsAff, o.doAfterUpdateHooks(ctx, exec) +} + +// UpdateAll updates all rows with the specified column values. +func (q fallbackResetTokenQuery) 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 fallback_reset_tokens") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected for fallback_reset_tokens") + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o FallbackResetTokenSlice) 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)), fallbackResetTokenPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"fallback_reset_tokens\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, fallbackResetTokenPrimaryKeyColumns, 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 fallbackResetToken slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all fallbackResetToken") + } + return rowsAff, nil +} + +// Delete deletes a single FallbackResetToken record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *FallbackResetToken) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if o == nil { + return 0, errors.New("models: no FallbackResetToken provided for delete") + } + + if err := o.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), fallbackResetTokenPrimaryKeyMapping) + sql := "DELETE FROM \"fallback_reset_tokens\" 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 fallback_reset_tokens") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by delete for fallback_reset_tokens") + } + + if err := o.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + return rowsAff, nil +} + +// DeleteAll deletes all matching rows. +func (q fallbackResetTokenQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if q.Query == nil { + return 0, errors.New("models: no fallbackResetTokenQuery 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 fallback_reset_tokens") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for fallback_reset_tokens") + } + + return rowsAff, nil +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o FallbackResetTokenSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + if len(fallbackResetTokenBeforeDeleteHooks) != 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)), fallbackResetTokenPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "DELETE FROM \"fallback_reset_tokens\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, fallbackResetTokenPrimaryKeyColumns, 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 fallbackResetToken slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for fallback_reset_tokens") + } + + if len(fallbackResetTokenAfterDeleteHooks) != 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 *FallbackResetToken) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindFallbackResetToken(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 *FallbackResetTokenSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := FallbackResetTokenSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), fallbackResetTokenPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"fallback_reset_tokens\".* FROM \"fallback_reset_tokens\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, fallbackResetTokenPrimaryKeyColumns, 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 FallbackResetTokenSlice") + } + + *o = slice + + return nil +} + +// FallbackResetTokenExists checks if the FallbackResetToken row exists. +func FallbackResetTokenExists(ctx context.Context, exec boil.ContextExecutor, iD int64) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"fallback_reset_tokens\" 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 fallback_reset_tokens exists") + } + + return exists, nil +} diff --git a/roomdb/sqlite/models/members.go b/roomdb/sqlite/models/members.go index 91f0e9a..75e2a37 100644 --- a/roomdb/sqlite/models/members.go +++ b/roomdb/sqlite/models/members.go @@ -55,23 +55,29 @@ var MemberWhere = struct { // MemberRels is where relationship names are stored. var MemberRels = struct { - SIWSSBSessions string - Aliases string - FallbackPasswords string - CreatedByInvites string + FallbackPassword string + SIWSSBSessions string + Aliases string + ForMemberFallbackResetTokens string + CreatedByFallbackResetTokens string + CreatedByInvites string }{ - SIWSSBSessions: "SIWSSBSessions", - Aliases: "Aliases", - FallbackPasswords: "FallbackPasswords", - CreatedByInvites: "CreatedByInvites", + FallbackPassword: "FallbackPassword", + SIWSSBSessions: "SIWSSBSessions", + Aliases: "Aliases", + ForMemberFallbackResetTokens: "ForMemberFallbackResetTokens", + CreatedByFallbackResetTokens: "CreatedByFallbackResetTokens", + CreatedByInvites: "CreatedByInvites", } // memberR is where relationships are stored. type memberR struct { - SIWSSBSessions SIWSSBSessionSlice `boil:"SIWSSBSessions" json:"SIWSSBSessions" toml:"SIWSSBSessions" yaml:"SIWSSBSessions"` - Aliases AliasSlice `boil:"Aliases" json:"Aliases" toml:"Aliases" yaml:"Aliases"` - FallbackPasswords FallbackPasswordSlice `boil:"FallbackPasswords" json:"FallbackPasswords" toml:"FallbackPasswords" yaml:"FallbackPasswords"` - CreatedByInvites InviteSlice `boil:"CreatedByInvites" json:"CreatedByInvites" toml:"CreatedByInvites" yaml:"CreatedByInvites"` + FallbackPassword *FallbackPassword `boil:"FallbackPassword" json:"FallbackPassword" toml:"FallbackPassword" yaml:"FallbackPassword"` + SIWSSBSessions SIWSSBSessionSlice `boil:"SIWSSBSessions" json:"SIWSSBSessions" toml:"SIWSSBSessions" yaml:"SIWSSBSessions"` + Aliases AliasSlice `boil:"Aliases" json:"Aliases" toml:"Aliases" yaml:"Aliases"` + ForMemberFallbackResetTokens FallbackResetTokenSlice `boil:"ForMemberFallbackResetTokens" json:"ForMemberFallbackResetTokens" toml:"ForMemberFallbackResetTokens" yaml:"ForMemberFallbackResetTokens"` + CreatedByFallbackResetTokens FallbackResetTokenSlice `boil:"CreatedByFallbackResetTokens" json:"CreatedByFallbackResetTokens" toml:"CreatedByFallbackResetTokens" yaml:"CreatedByFallbackResetTokens"` + CreatedByInvites InviteSlice `boil:"CreatedByInvites" json:"CreatedByInvites" toml:"CreatedByInvites" yaml:"CreatedByInvites"` } // NewStruct creates a new relationship struct @@ -364,6 +370,20 @@ func (q memberQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (boo return count > 0, nil } +// FallbackPassword pointed to by the foreign key. +func (o *Member) FallbackPassword(mods ...qm.QueryMod) fallbackPasswordQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"member_id\" = ?", o.ID), + } + + queryMods = append(queryMods, mods...) + + query := FallbackPasswords(queryMods...) + queries.SetFrom(query.Query, "\"fallback_passwords\"") + + return query +} + // SIWSSBSessions retrieves all the SIWSSB_session's SIWSSBSessions with an executor. func (o *Member) SIWSSBSessions(mods ...qm.QueryMod) sIWSSBSessionQuery { var queryMods []qm.QueryMod @@ -406,22 +426,43 @@ func (o *Member) Aliases(mods ...qm.QueryMod) aliasQuery { return query } -// FallbackPasswords retrieves all the fallback_password's FallbackPasswords with an executor. -func (o *Member) FallbackPasswords(mods ...qm.QueryMod) fallbackPasswordQuery { +// ForMemberFallbackResetTokens retrieves all the fallback_reset_token's FallbackResetTokens with an executor via for_member column. +func (o *Member) ForMemberFallbackResetTokens(mods ...qm.QueryMod) fallbackResetTokenQuery { var queryMods []qm.QueryMod if len(mods) != 0 { queryMods = append(queryMods, mods...) } queryMods = append(queryMods, - qm.Where("\"fallback_passwords\".\"member_id\"=?", o.ID), + qm.Where("\"fallback_reset_tokens\".\"for_member\"=?", o.ID), ) - query := FallbackPasswords(queryMods...) - queries.SetFrom(query.Query, "\"fallback_passwords\"") + query := FallbackResetTokens(queryMods...) + queries.SetFrom(query.Query, "\"fallback_reset_tokens\"") if len(queries.GetSelect(query.Query)) == 0 { - queries.SetSelect(query.Query, []string{"\"fallback_passwords\".*"}) + queries.SetSelect(query.Query, []string{"\"fallback_reset_tokens\".*"}) + } + + return query +} + +// CreatedByFallbackResetTokens retrieves all the fallback_reset_token's FallbackResetTokens with an executor via created_by column. +func (o *Member) CreatedByFallbackResetTokens(mods ...qm.QueryMod) fallbackResetTokenQuery { + var queryMods []qm.QueryMod + if len(mods) != 0 { + queryMods = append(queryMods, mods...) + } + + queryMods = append(queryMods, + qm.Where("\"fallback_reset_tokens\".\"created_by\"=?", o.ID), + ) + + query := FallbackResetTokens(queryMods...) + queries.SetFrom(query.Query, "\"fallback_reset_tokens\"") + + if len(queries.GetSelect(query.Query)) == 0 { + queries.SetSelect(query.Query, []string{"\"fallback_reset_tokens\".*"}) } return query @@ -448,6 +489,107 @@ func (o *Member) CreatedByInvites(mods ...qm.QueryMod) inviteQuery { return query } +// LoadFallbackPassword allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for a 1-1 relationship. +func (memberL) LoadFallbackPassword(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error { + var slice []*Member + var object *Member + + if singular { + object = maybeMember.(*Member) + } else { + slice = *maybeMember.(*[]*Member) + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &memberR{} + } + args = append(args, object.ID) + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &memberR{} + } + + 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(`fallback_passwords`), + qm.WhereIn(`fallback_passwords.member_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 FallbackPassword") + } + + var resultSlice []*FallbackPassword + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice FallbackPassword") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for fallback_passwords") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for fallback_passwords") + } + + if len(memberAfterSelectHooks) != 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.FallbackPassword = foreign + if foreign.R == nil { + foreign.R = &fallbackPasswordR{} + } + foreign.R.Member = object + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if local.ID == foreign.MemberID { + local.R.FallbackPassword = foreign + if foreign.R == nil { + foreign.R = &fallbackPasswordR{} + } + foreign.R.Member = local + break + } + } + } + + return nil +} + // LoadSIWSSBSessions 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 (memberL) LoadSIWSSBSessions(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error { @@ -644,9 +786,9 @@ func (memberL) LoadAliases(ctx context.Context, e boil.ContextExecutor, singular return nil } -// LoadFallbackPasswords allows an eager lookup of values, cached into the +// LoadForMemberFallbackResetTokens 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 (memberL) LoadFallbackPasswords(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error { +func (memberL) LoadForMemberFallbackResetTokens(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error { var slice []*Member var object *Member @@ -684,8 +826,8 @@ func (memberL) LoadFallbackPasswords(ctx context.Context, e boil.ContextExecutor } query := NewQuery( - qm.From(`fallback_passwords`), - qm.WhereIn(`fallback_passwords.member_id in ?`, args...), + qm.From(`fallback_reset_tokens`), + qm.WhereIn(`fallback_reset_tokens.for_member in ?`, args...), ) if mods != nil { mods.Apply(query) @@ -693,22 +835,22 @@ func (memberL) LoadFallbackPasswords(ctx context.Context, e boil.ContextExecutor results, err := query.QueryContext(ctx, e) if err != nil { - return errors.Wrap(err, "failed to eager load fallback_passwords") + return errors.Wrap(err, "failed to eager load fallback_reset_tokens") } - var resultSlice []*FallbackPassword + var resultSlice []*FallbackResetToken if err = queries.Bind(results, &resultSlice); err != nil { - return errors.Wrap(err, "failed to bind eager loaded slice fallback_passwords") + return errors.Wrap(err, "failed to bind eager loaded slice fallback_reset_tokens") } if err = results.Close(); err != nil { - return errors.Wrap(err, "failed to close results in eager load on fallback_passwords") + return errors.Wrap(err, "failed to close results in eager load on fallback_reset_tokens") } if err = results.Err(); err != nil { - return errors.Wrap(err, "error occurred during iteration of eager loaded relations for fallback_passwords") + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for fallback_reset_tokens") } - if len(fallbackPasswordAfterSelectHooks) != 0 { + if len(fallbackResetTokenAfterSelectHooks) != 0 { for _, obj := range resultSlice { if err := obj.doAfterSelectHooks(ctx, e); err != nil { return err @@ -716,24 +858,122 @@ func (memberL) LoadFallbackPasswords(ctx context.Context, e boil.ContextExecutor } } if singular { - object.R.FallbackPasswords = resultSlice + object.R.ForMemberFallbackResetTokens = resultSlice for _, foreign := range resultSlice { if foreign.R == nil { - foreign.R = &fallbackPasswordR{} + foreign.R = &fallbackResetTokenR{} } - foreign.R.Member = object + foreign.R.ForMemberMember = object } return nil } for _, foreign := range resultSlice { for _, local := range slice { - if local.ID == foreign.MemberID { - local.R.FallbackPasswords = append(local.R.FallbackPasswords, foreign) + if local.ID == foreign.ForMember { + local.R.ForMemberFallbackResetTokens = append(local.R.ForMemberFallbackResetTokens, foreign) if foreign.R == nil { - foreign.R = &fallbackPasswordR{} + foreign.R = &fallbackResetTokenR{} } - foreign.R.Member = local + foreign.R.ForMemberMember = local + break + } + } + } + + return nil +} + +// LoadCreatedByFallbackResetTokens 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 (memberL) LoadCreatedByFallbackResetTokens(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error { + var slice []*Member + var object *Member + + if singular { + object = maybeMember.(*Member) + } else { + slice = *maybeMember.(*[]*Member) + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &memberR{} + } + args = append(args, object.ID) + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &memberR{} + } + + 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(`fallback_reset_tokens`), + qm.WhereIn(`fallback_reset_tokens.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 fallback_reset_tokens") + } + + var resultSlice []*FallbackResetToken + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice fallback_reset_tokens") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results in eager load on fallback_reset_tokens") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for fallback_reset_tokens") + } + + if len(fallbackResetTokenAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + if singular { + object.R.CreatedByFallbackResetTokens = resultSlice + for _, foreign := range resultSlice { + if foreign.R == nil { + foreign.R = &fallbackResetTokenR{} + } + foreign.R.CreatedByMember = object + } + return nil + } + + for _, foreign := range resultSlice { + for _, local := range slice { + if local.ID == foreign.CreatedBy { + local.R.CreatedByFallbackResetTokens = append(local.R.CreatedByFallbackResetTokens, foreign) + if foreign.R == nil { + foreign.R = &fallbackResetTokenR{} + } + foreign.R.CreatedByMember = local break } } @@ -840,6 +1080,57 @@ func (memberL) LoadCreatedByInvites(ctx context.Context, e boil.ContextExecutor, return nil } +// SetFallbackPassword of the member to the related item. +// Sets o.R.FallbackPassword to related. +// Adds o to related.R.Member. +func (o *Member) SetFallbackPassword(ctx context.Context, exec boil.ContextExecutor, insert bool, related *FallbackPassword) error { + var err error + + if insert { + related.MemberID = o.ID + + if err = related.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } else { + updateQuery := fmt.Sprintf( + "UPDATE \"fallback_passwords\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, []string{"member_id"}), + strmangle.WhereClause("\"", "\"", 0, fallbackPasswordPrimaryKeyColumns), + ) + values := []interface{}{o.ID, related.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") + } + + related.MemberID = o.ID + + } + + if o.R == nil { + o.R = &memberR{ + FallbackPassword: related, + } + } else { + o.R.FallbackPassword = related + } + + if related.R == nil { + related.R = &fallbackPasswordR{ + Member: o, + } + } else { + related.R.Member = o + } + return nil +} + // AddSIWSSBSessions adds the given related objects to the existing relationships // of the member, optionally inserting them as new records. // Appends related to o.R.SIWSSBSessions. @@ -946,23 +1237,23 @@ func (o *Member) AddAliases(ctx context.Context, exec boil.ContextExecutor, inse return nil } -// AddFallbackPasswords adds the given related objects to the existing relationships +// AddForMemberFallbackResetTokens adds the given related objects to the existing relationships // of the member, optionally inserting them as new records. -// Appends related to o.R.FallbackPasswords. -// Sets related.R.Member appropriately. -func (o *Member) AddFallbackPasswords(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*FallbackPassword) error { +// Appends related to o.R.ForMemberFallbackResetTokens. +// Sets related.R.ForMemberMember appropriately. +func (o *Member) AddForMemberFallbackResetTokens(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*FallbackResetToken) error { var err error for _, rel := range related { if insert { - rel.MemberID = o.ID + rel.ForMember = 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 \"fallback_passwords\" SET %s WHERE %s", - strmangle.SetParamNames("\"", "\"", 0, []string{"member_id"}), - strmangle.WhereClause("\"", "\"", 0, fallbackPasswordPrimaryKeyColumns), + "UPDATE \"fallback_reset_tokens\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, []string{"for_member"}), + strmangle.WhereClause("\"", "\"", 0, fallbackResetTokenPrimaryKeyColumns), ) values := []interface{}{o.ID, rel.ID} @@ -975,25 +1266,78 @@ func (o *Member) AddFallbackPasswords(ctx context.Context, exec boil.ContextExec return errors.Wrap(err, "failed to update foreign table") } - rel.MemberID = o.ID + rel.ForMember = o.ID } } if o.R == nil { o.R = &memberR{ - FallbackPasswords: related, + ForMemberFallbackResetTokens: related, } } else { - o.R.FallbackPasswords = append(o.R.FallbackPasswords, related...) + o.R.ForMemberFallbackResetTokens = append(o.R.ForMemberFallbackResetTokens, related...) } for _, rel := range related { if rel.R == nil { - rel.R = &fallbackPasswordR{ - Member: o, + rel.R = &fallbackResetTokenR{ + ForMemberMember: o, } } else { - rel.R.Member = o + rel.R.ForMemberMember = o + } + } + return nil +} + +// AddCreatedByFallbackResetTokens adds the given related objects to the existing relationships +// of the member, optionally inserting them as new records. +// Appends related to o.R.CreatedByFallbackResetTokens. +// Sets related.R.CreatedByMember appropriately. +func (o *Member) AddCreatedByFallbackResetTokens(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*FallbackResetToken) 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 \"fallback_reset_tokens\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, []string{"created_by"}), + strmangle.WhereClause("\"", "\"", 0, fallbackResetTokenPrimaryKeyColumns), + ) + 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 = &memberR{ + CreatedByFallbackResetTokens: related, + } + } else { + o.R.CreatedByFallbackResetTokens = append(o.R.CreatedByFallbackResetTokens, related...) + } + + for _, rel := range related { + if rel.R == nil { + rel.R = &fallbackResetTokenR{ + CreatedByMember: o, + } + } else { + rel.R.CreatedByMember = o } } return nil diff --git a/roomdb/sqlite/new.go b/roomdb/sqlite/new.go index 8485d14..058e9eb 100644 --- a/roomdb/sqlite/new.go +++ b/roomdb/sqlite/new.go @@ -82,11 +82,19 @@ func Open(r repo.Interface) (*Database, error) { return nil, err } + if err := deleteConsumedResetTokens(db); err != nil { + return nil, err + } + + // scrub old invites and reset tokens go func() { // server might not restart as often - threeDays := 5 * 24 * time.Hour - ticker := time.NewTicker(threeDays) + fiveDays := 5 * 24 * time.Hour + ticker := time.NewTicker(fiveDays) for range ticker.C { err := transact(db, func(tx *sql.Tx) error { + if err := deleteConsumedResetTokens(tx); err != nil { + return err + } return deleteConsumedInvites(tx) }) if err != nil { diff --git a/roomdb/sqlite/new_test.go b/roomdb/sqlite/new_test.go index 50a2c1a..5394876 100644 --- a/roomdb/sqlite/new_test.go +++ b/roomdb/sqlite/new_test.go @@ -1,8 +1,6 @@ package sqlite import ( - "bytes" - "context" "os" "path/filepath" "testing" @@ -11,8 +9,6 @@ import ( "github.com/stretchr/testify/require" "github.com/ssb-ngi-pointer/go-ssb-room/internal/repo" - "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" - refs "go.mindeco.de/ssb-refs" ) // verify the database opens and migrates successfully from zero state @@ -28,25 +24,3 @@ func TestSchema(t *testing.T) { err = db.Close() require.NoError(t, err) } - -func TestBasic(t *testing.T) { - testRepo := filepath.Join("testrun", t.Name()) - os.RemoveAll(testRepo) - - tr := repo.New(testRepo) - - db, err := Open(tr) - require.NoError(t, err) - - ctx := context.Background() - feedA := refs.FeedRef{ID: bytes.Repeat([]byte("1312"), 8), Algo: refs.RefAlgoFeedSSB1} - memberID, err := db.Members.Add(ctx, feedA, roomdb.RoleMember) - require.NoError(t, err) - require.NotEqual(t, 0, memberID) - - err = db.AuthFallback.Create(ctx, memberID, "testLogin", []byte("super-cheesy-password-12345")) - require.NoError(t, err) - - err = db.Close() - require.NoError(t, err) -} diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go index 86770f5..e159263 100644 --- a/web/handlers/admin/handler.go +++ b/web/handlers/admin/handler.go @@ -47,11 +47,13 @@ var HTMLTemplates = []string{ "admin/member.tmpl", "admin/member-list.tmpl", "admin/members-remove-confirm.tmpl", + "admin/members-show-password-reset-token.tmpl", } // Databases is an option struct that encapsulates the required database services type Databases struct { Aliases roomdb.AliasesService + AuthFallback roomdb.AuthFallbackService Config roomdb.RoomConfig DeniedKeys roomdb.DeniedKeysService Invites roomdb.InvitesService @@ -125,6 +127,8 @@ func Handler( netInfo: netInfo, db: dbs.Members, + + fallbackAuthDB: dbs.AuthFallback, } mux.HandleFunc("/member", r.HTML("admin/member.tmpl", mh.details)) mux.HandleFunc("/members", r.HTML("admin/member-list.tmpl", mh.overview)) @@ -132,6 +136,7 @@ func Handler( mux.HandleFunc("/members/change-role", mh.changeRole) mux.HandleFunc("/members/remove/confirm", r.HTML("admin/members-remove-confirm.tmpl", mh.removeConfirm)) mux.HandleFunc("/members/remove", mh.remove) + mux.HandleFunc("/members/create-fallback-reset-link", r.HTML("admin/members-show-password-reset-token.tmpl", mh.createPasswordResetToken)) var ih = invitesHandler{ r: r, diff --git a/web/handlers/admin/members.go b/web/handlers/admin/members.go index cab0f1f..5a8a2a1 100644 --- a/web/handlers/admin/members.go +++ b/web/handlers/admin/members.go @@ -27,7 +27,8 @@ type membersHandler struct { urlTo web.URLMaker netInfo network.ServerEndpointDetails - db roomdb.MembersService + db roomdb.MembersService + fallbackAuthDB roomdb.AuthFallbackService } const redirectToMembers = "/admin/members" @@ -219,3 +220,36 @@ func (h membersHandler) remove(rw http.ResponseWriter, req *http.Request) { http.Redirect(rw, req, redirectToMembers, http.StatusTemporaryRedirect) } + +func (h membersHandler) createPasswordResetToken(rw http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "POST" { + return nil, weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)} + } + + if err := req.ParseForm(); err != nil { + return nil, weberrors.ErrBadRequest{Where: "Form data", Details: err} + } + + forMemberID, err := strconv.ParseInt(req.FormValue("member_id"), 10, 64) + if err != nil { + err = weberrors.ErrBadRequest{Where: "Member ID", Details: err} + return nil, weberrors.ErrRedirect{Path: redirectToMembers, Reason: err} + } + + creatingMember := members.FromContext(req.Context()) + if creatingMember == nil || creatingMember.Role != roomdb.RoleAdmin { + err = weberrors.ErrForbidden{Details: fmt.Errorf("not an admin")} + return nil, weberrors.ErrRedirect{Path: redirectToMembers, Reason: err} + } + + token, err := h.fallbackAuthDB.CreateResetToken(req.Context(), creatingMember.ID, forMemberID) + if err != nil { + return nil, weberrors.ErrRedirect{Path: redirectToMembers, Reason: err} + } + + resetFormURL := h.urlTo(router.MembersChangePasswordForm, "token", token) + + return map[string]interface{}{ + "ResetLinkURL": template.URL(resetFormURL.String()), + }, nil +} diff --git a/web/handlers/http.go b/web/handlers/http.go index a0528fe..0892903 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -291,6 +291,7 @@ func New( locHelper, admin.Databases{ Aliases: dbs.Aliases, + AuthFallback: dbs.AuthFallback, Config: dbs.Config, DeniedKeys: dbs.DeniedKeys, Invites: dbs.Invites, @@ -302,7 +303,8 @@ func New( mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler)) m.Get(router.MembersChangePasswordForm).HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if members.FromContext(req.Context()) == nil { + resetToken := req.URL.Query().Get("token") + if members.FromContext(req.Context()) == nil && resetToken == "" { r.Error(w, req, http.StatusUnauthorized, weberrs.ErrNotAuthorized) return } @@ -316,7 +318,7 @@ func New( return } - // TODO: add resetToken to render + pageData["ResetToken"] = resetToken err = r.Render(w, req, "change-member-password.tmpl", http.StatusOK, pageData) if err != nil { @@ -325,18 +327,42 @@ func New( }) m.Get(router.MembersChangePassword).HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - redirectURL := req.Header.Get("Referer") + var ( + ctx = req.Context() + memberID = int64(-1) + redirectURL = req.Header.Get("Referer") + + resetToken string + ) + if redirectURL == "" { http.Error(w, "TODO: add correct redirect handling", http.StatusInternalServerError) return } - err := req.ParseForm() - if err != nil { - r.Error(w, req, http.StatusInternalServerError, err) + if req.Method != http.MethodPost { + r.Error(w, req, http.StatusBadRequest, fmt.Errorf("expected POST method")) return } + err := req.ParseForm() + if err != nil { + r.Error(w, req, http.StatusBadRequest, err) + return + } + + resetToken = req.FormValue("reset-token") + if m := members.FromContext(ctx); m != nil { + memberID = m.ID + + // shouldn't have both token and logged in user + if resetToken != "" { + r.Error(w, req, http.StatusBadRequest, fmt.Errorf("can't have logged in user and reset-token present. Log out and try again")) + return + } + } + + // check the passwords match and it hasnt been pwned repeat := req.FormValue("repeat-password") newpw := req.FormValue("new-password") @@ -364,8 +390,22 @@ func New( return } - fmt.Fprintln(w, "password looks okay!") - // TODO: update password db + // update the password + if resetToken == "" { + err = dbs.AuthFallback.SetPassword(ctx, memberID, []byte(newpw)) + } else { + err = dbs.AuthFallback.SetPasswordWithToken(ctx, resetToken, []byte(newpw)) + } + + // add flash msg about the outcome and redirect the user + if err != nil { + flashHelper.AddError(w, req, err) + } else { + flashHelper.AddMessage(w, req, "AuthFallbackPasswordUpdated") + } + + redirectURL = urlTo(router.AuthFallbackLogin).Path + http.Redirect(w, req, redirectURL, http.StatusSeeOther) }) // handle setting language diff --git a/web/i18n/defaults/active.de.toml b/web/i18n/defaults/active.de.toml index d17d017..900abae 100644 --- a/web/i18n/defaults/active.de.toml +++ b/web/i18n/defaults/active.de.toml @@ -55,8 +55,13 @@ AuthFallbackTitle = "Passwort anmelden" AuthFallbackWelcome = "Eine Anmeldung mit Benutzername und Passwort ist nur möglich, wenn der Administrator Ihnen eines gegeben hat, da wir die Benutzerregistrierung nicht unterstützen." AuthFallbackInstruct = "Diese Methode ist ein akzeptabler Fallback, wenn Sie einen Benutzernamen und ein Passwort haben." -AuthFallbackPasswordChangeFormTitle = "Change Password" -AuthFallbackPasswordChangeWelcome = "Here you can change your fallback password. Make sure it's longer then 10 characters and that they match." +AuthFallbackNewPassword="Neues Passwort" +AuthFallbackRepeatPassword="Passwort wiederholen" +AuthFallbackPasswordChangeFormTitle = "Passwort ändern" +AuthFallbackPasswordChangeWelcome = "Hier können Sie ihr Passwort neu setzen. Bitte achten sie darauf, dass es länger als 10 Zeichen ist. Durch die doppelte eingabe wird sichergestellt, dass Sie sich nicht vertippt haben. Auserdem verwenden wir die Datenbank von haveibeenpwned.com um sicher zu stellen, dass Sie kein unsicherers Passwort verwenden, ohne es zu wissen." +AuthFallbackPasswordUpdated = "Das Passwort wurde aktualisiert. Sie können sich nun damit Einloggen." +AdminMemberPasswordResetLinkCreatedTitle = "Passwort reset link erstellt" +AdminMemberPasswordResetLinkCreatedInstruct = "Der reset Link wurde erstellt. Bitte senden Sie ihn, uber einen geeigneten Seitenkanal wie z.B. E-Mail, an das Mitglied. Wenn die Person den Link offnet, ist Sie in der Lage sich ein neues Passwort zu geben." # general dashboard stuff ######################### diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index 5971cca..e8e9364 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -62,8 +62,13 @@ AuthFallbackTitle = "Password sign-in" AuthFallbackWelcome = "Signing in with username and password is only possible if the administrator has given you one, because we do not support user registration." AuthFallbackInstruct = "This method is an acceptable fallback, if you have a username and password." +AuthFallbackNewPassword="New Password" +AuthFallbackRepeatPassword="Repeat Password" AuthFallbackPasswordChangeFormTitle = "Change Password" -AuthFallbackPasswordChangeWelcome = "Here you can change your fallback password. Make sure it's longer then 10 characters and that they match." +AuthFallbackPasswordChangeWelcome = "Here you can change your fallback password. Please make sure it's longer then 10 characters. Via the repition we make sure that you don't accidentally mistype it. Additionally we use the lookup from haveibeenpwned.com to make sure you don't accidentally use a weak password." +AuthFallbackPasswordUpdated = "The password was updated. You can now use it to sign in." +AdminMemberPasswordResetLinkCreatedTitle = "Password reset token created" +AdminMemberPasswordResetLinkCreatedInstruct = "The reset token was created. Please send it to the member via some means (like E-Mail or another suitable side-channel). When they open it, they will be able to choose a new password for themselves." # general dashboard stuff ######################### diff --git a/web/templates/admin/member.tmpl b/web/templates/admin/member.tmpl index e21e418..96f3fa3 100644 --- a/web/templates/admin/member.tmpl +++ b/web/templates/admin/member.tmpl @@ -89,8 +89,9 @@ >{{i18n "AdminMemberDetailsChangePassword"}} {{ else if member_is_elevated }} -
+ {{ .csrfField }} + diff --git a/web/templates/admin/members-show-password-reset-token.tmpl b/web/templates/admin/members-show-password-reset-token.tmpl new file mode 100644 index 0000000..9dc0163 --- /dev/null +++ b/web/templates/admin/members-show-password-reset-token.tmpl @@ -0,0 +1,19 @@ +{{ define "title" }}{{i18n "AdminMemberPasswordResetLinkCreatedTitle"}}{{ end }} +{{ define "content" }} +
+ + + + + {{i18n "AdminMemberPasswordResetLinkCreatedTitle"}}
{{i18n "AdminMemberPasswordResetLinkCreatedInstruct"}}
+ + {{.ResetLinkURL}} +
+{{end}} \ No newline at end of file diff --git a/web/templates/change-member-password.tmpl b/web/templates/change-member-password.tmpl index d328181..bdf93ee 100644 --- a/web/templates/change-member-password.tmpl +++ b/web/templates/change-member-password.tmpl @@ -1,7 +1,7 @@ {{ define "title" }}{{ i18n "AuthFallbackPasswordChangeFormTitle" }}{{ end }} {{ define "content" }}
- {{i18n "AuthFallbackPasswordChangeWelcome"}} + {{i18n "AuthFallbackPasswordChangeWelcome"}} {{ template "flashes" . }} @@ -11,15 +11,22 @@ method="POST" class="flex flex-col items-center self-stretch" > - {{.csrfField}} - + {{.csrfField}} + {{if ne .ResetToken ""}} + + {{end}} + + - {{i18n "AuthFallbackRepeatPassword"}} +