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
This commit is contained in:
Henry 2021-05-11 11:05:17 +02:00
parent 5bfb5316f8
commit 4558b208ee
24 changed files with 2340 additions and 240 deletions

View File

@ -27,7 +27,7 @@ jobs:
run: go get -v -t -d ./... run: go get -v -t -d ./...
- name: Build smoke test - name: Build smoke test
run: go build ./cmd/server run: go build ./cmd/...
- name: install node ssb-stack - name: install node ssb-stack
run: | run: |

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT // 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 package main
import ( import (
@ -28,24 +28,10 @@ func main() {
check(err) check(err)
var ( var (
login string
pubKey *refs.FeedRef
role roomdb.Role = roomdb.RoleAdmin role roomdb.Role = roomdb.RoleAdmin
repoPath string 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: @<base64-encoded public-key>.ed25519", func(val string) error {
if len(val) == 0 {
return fmt.Errorf("the public key is required. if you are just testing things out, generate one by running 'cmd/insert-user/generate-fake-id.sh'\n")
}
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.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 { 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) { switch strings.ToLower(val) {
@ -65,18 +51,15 @@ func main() {
}) })
flag.Parse() flag.Parse()
/* we require at least 5 arguments: <executable> + -login <val> + -key <val> */ // we require one more argument which is not a flag.
/* 1 2 3 4 5 */ if len(flag.Args()) != 1 {
if len(os.Args) < 5 { cliMissingArguments("please provide a public key")
cliMissingArguments("please provide the default arguments -login and -key")
} }
if login == "" { pubKey, err := refs.ParseFeedRef(flag.Arg(0))
cliMissingArguments("please provide a username with -login <username>") if err != nil {
} fmt.Fprintln(os.Stderr, "Invalid ssb public-key referenfce:", err)
os.Exit(1)
if pubKey == nil {
cliMissingArguments("please provide a public key with -key")
} }
r := repo.New(repoPath) r := repo.New(repoPath)
@ -95,22 +78,21 @@ func main() {
if !bytes.Equal(bytePassword, bytePasswordRepeat) { if !bytes.Equal(bytePassword, bytePasswordRepeat) {
fmt.Fprintln(os.Stderr, "Passwords didn't match") fmt.Fprintln(os.Stderr, "Passwords didn't match")
os.Exit(1) os.Exit(1)
return
} }
ctx := context.Background() ctx := context.Background()
mid, err := db.Members.Add(ctx, *pubKey, role) mid, err := db.Members.Add(ctx, *pubKey, role)
check(err) check(err)
err = db.AuthFallback.Create(ctx, mid, login, bytePassword) err = db.AuthFallback.SetPassword(ctx, mid, string(bytePassword))
check(err) 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) { func cliMissingArguments(message string) {
executable := strings.TrimPrefix(os.Args[0], "./") executable := strings.TrimPrefix(os.Args[0], "./")
fmt.Fprintf(os.Stderr, "%s: %s\nusage:%s -login <login-name> -key <@<base64-encoded public key>.ed25519> <optional flags>\n", executable, message, executable) fmt.Fprintf(os.Stderr, "%s: %s\nusage:%s <@base64-encoded-public-key=.ed25519> <optional flags>\n", executable, message, executable)
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }

View File

@ -29,14 +29,18 @@ type RoomConfig interface {
type AuthFallbackService interface { type AuthFallbackService interface {
// Check receives the username and password (in clear) and checks them accordingly. // 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. // If it's a valid combination it returns the user ID, or an error if they are not.
auth.Auther auth.Auther
Create(_ context.Context, memberID int64, login string, password []byte) error // SetPassword creates or updates a fallback login password for this user.
// GetByID(context.Context, int64) (User, error) SetPassword(_ context.Context, memberID int64, password []byte) error
// ListAll()
// ListByMember() // CreateResetToken returns a token which can be used via SetPasswordWithToken() to reset the password of a member.
// Remove(pwid) 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 // AuthWithSSBService defines utility functions for the challenge/response system of sign-in with ssb

View File

@ -23,18 +23,45 @@ type FakeAuthFallbackService struct {
result1 interface{} result1 interface{}
result2 error result2 error
} }
CreateStub func(context.Context, int64, string, []byte) error CreateResetTokenStub func(context.Context, int64, int64) (string, error)
createMutex sync.RWMutex createResetTokenMutex sync.RWMutex
createArgsForCall []struct { createResetTokenArgsForCall []struct {
arg1 context.Context arg1 context.Context
arg2 int64 arg2 int64
arg3 string arg3 int64
arg4 []byte
} }
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 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 result1 error
} }
invocations map[string][][]interface{} invocations map[string][][]interface{}
@ -106,26 +133,91 @@ func (fake *FakeAuthFallbackService) CheckReturnsOnCall(i int, result1 interface
}{result1, result2} }{result1, result2}
} }
func (fake *FakeAuthFallbackService) Create(arg1 context.Context, arg2 int64, arg3 string, arg4 []byte) error { func (fake *FakeAuthFallbackService) CreateResetToken(arg1 context.Context, arg2 int64, arg3 int64) (string, error) {
var arg4Copy []byte fake.createResetTokenMutex.Lock()
if arg4 != nil { ret, specificReturn := fake.createResetTokenReturnsOnCall[len(fake.createResetTokenArgsForCall)]
arg4Copy = make([]byte, len(arg4)) fake.createResetTokenArgsForCall = append(fake.createResetTokenArgsForCall, struct {
copy(arg4Copy, arg4)
}
fake.createMutex.Lock()
ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)]
fake.createArgsForCall = append(fake.createArgsForCall, struct {
arg1 context.Context arg1 context.Context
arg2 int64 arg2 int64
arg3 string arg3 int64
arg4 []byte }{arg1, arg2, arg3})
}{arg1, arg2, arg3, arg4Copy}) stub := fake.CreateResetTokenStub
stub := fake.CreateStub fakeReturns := fake.createResetTokenReturns
fakeReturns := fake.createReturns fake.recordInvocation("CreateResetToken", []interface{}{arg1, arg2, arg3})
fake.recordInvocation("Create", []interface{}{arg1, arg2, arg3, arg4Copy}) fake.createResetTokenMutex.Unlock()
fake.createMutex.Unlock()
if stub != nil { 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 { if specificReturn {
return ret.result1 return ret.result1
@ -133,44 +225,112 @@ func (fake *FakeAuthFallbackService) Create(arg1 context.Context, arg2 int64, ar
return fakeReturns.result1 return fakeReturns.result1
} }
func (fake *FakeAuthFallbackService) CreateCallCount() int { func (fake *FakeAuthFallbackService) SetPasswordCallCount() int {
fake.createMutex.RLock() fake.setPasswordMutex.RLock()
defer fake.createMutex.RUnlock() defer fake.setPasswordMutex.RUnlock()
return len(fake.createArgsForCall) return len(fake.setPasswordArgsForCall)
} }
func (fake *FakeAuthFallbackService) CreateCalls(stub func(context.Context, int64, string, []byte) error) { func (fake *FakeAuthFallbackService) SetPasswordCalls(stub func(context.Context, int64, []byte) error) {
fake.createMutex.Lock() fake.setPasswordMutex.Lock()
defer fake.createMutex.Unlock() defer fake.setPasswordMutex.Unlock()
fake.CreateStub = stub fake.SetPasswordStub = stub
} }
func (fake *FakeAuthFallbackService) CreateArgsForCall(i int) (context.Context, int64, string, []byte) { func (fake *FakeAuthFallbackService) SetPasswordArgsForCall(i int) (context.Context, int64, []byte) {
fake.createMutex.RLock() fake.setPasswordMutex.RLock()
defer fake.createMutex.RUnlock() defer fake.setPasswordMutex.RUnlock()
argsForCall := fake.createArgsForCall[i] argsForCall := fake.setPasswordArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
} }
func (fake *FakeAuthFallbackService) CreateReturns(result1 error) { func (fake *FakeAuthFallbackService) SetPasswordReturns(result1 error) {
fake.createMutex.Lock() fake.setPasswordMutex.Lock()
defer fake.createMutex.Unlock() defer fake.setPasswordMutex.Unlock()
fake.CreateStub = nil fake.SetPasswordStub = nil
fake.createReturns = struct { fake.setPasswordReturns = struct {
result1 error result1 error
}{result1} }{result1}
} }
func (fake *FakeAuthFallbackService) CreateReturnsOnCall(i int, result1 error) { func (fake *FakeAuthFallbackService) SetPasswordReturnsOnCall(i int, result1 error) {
fake.createMutex.Lock() fake.setPasswordMutex.Lock()
defer fake.createMutex.Unlock() defer fake.setPasswordMutex.Unlock()
fake.CreateStub = nil fake.SetPasswordStub = nil
if fake.createReturnsOnCall == nil { if fake.setPasswordReturnsOnCall == nil {
fake.createReturnsOnCall = make(map[int]struct { fake.setPasswordReturnsOnCall = make(map[int]struct {
result1 error 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 error
}{result1} }{result1}
} }
@ -180,8 +340,12 @@ func (fake *FakeAuthFallbackService) Invocations() map[string][][]interface{} {
defer fake.invocationsMutex.RUnlock() defer fake.invocationsMutex.RUnlock()
fake.checkMutex.RLock() fake.checkMutex.RLock()
defer fake.checkMutex.RUnlock() defer fake.checkMutex.RUnlock()
fake.createMutex.RLock() fake.createResetTokenMutex.RLock()
defer fake.createMutex.RUnlock() defer fake.createResetTokenMutex.RUnlock()
fake.setPasswordMutex.RLock()
defer fake.setPasswordMutex.RUnlock()
fake.setPasswordWithTokenMutex.RLock()
defer fake.setPasswordWithTokenMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{} copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations { for key, value := range fake.invocations {
copiedInvocations[key] = value copiedInvocations[key] = value

View File

@ -4,10 +4,14 @@ package sqlite
import ( import (
"context" "context"
"crypto/rand"
"crypto/sha256"
"database/sql" "database/sql"
"encoding/base64"
"fmt" "fmt"
"github.com/friendsofgo/errors" "github.com/friendsofgo/errors"
"github.com/mattn/go-sqlite3"
"github.com/volatiletech/sqlboiler/v4/boil" "github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries/qm" "github.com/volatiletech/sqlboiler/v4/queries/qm"
"go.mindeco.de/http/auth" "go.mindeco.de/http/auth"
@ -30,15 +34,36 @@ var redirectPasswordAuthErr = weberrors.ErrRedirect{
Reason: auth.ErrBadLogin, 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. // 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) { func (af AuthFallback) Check(login, password string) (interface{}, error) {
ctx := context.Background() ctx := context.Background()
found, err := models.FallbackPasswords(
qm.Load("Member"), var memberID = int64(-1)
qm.Where("login = ?", login),
).One(ctx, af.db) // 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 err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, redirectPasswordAuthErr return nil, redirectPasswordAuthErr
@ -46,29 +71,161 @@ func (af AuthFallback) Check(login, password string) (interface{}, error) {
return nil, err return nil, err
} }
err = bcrypt.CompareHashAndPassword(found.PasswordHash, []byte(password)) err = bcrypt.CompareHashAndPassword(foundPassword.PasswordHash, []byte(password))
if err != nil { if err != nil {
return nil, redirectPasswordAuthErr 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 { func (af AuthFallback) SetPassword(ctx context.Context, memberID int64, password []byte) error {
var newPasswordEntry models.FallbackPassword
newPasswordEntry.MemberID = memberID
newPasswordEntry.Login = login
hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil { 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 { 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 return nil
} }

View File

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

View File

@ -7,7 +7,6 @@ import (
"database/sql" "database/sql"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"time"
"github.com/friendsofgo/errors" "github.com/friendsofgo/errors"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
@ -49,9 +48,6 @@ func (i Invites) Create(ctx context.Context, createdBy int64) (string, error) {
// generate an invite code // generate an invite code
rand.Read(tokenBytes) rand.Read(tokenBytes)
// see comment on migrations/6-invite-createdAt.sql
newInvite.CreatedAt = time.Now()
// hash the binary of the token for storage // hash the binary of the token for storage
h := sha256.New() h := sha256.New()
h.Write(tokenBytes) h.Write(tokenBytes)

View File

@ -60,17 +60,17 @@ CREATE INDEX denied_keys_by_pubkey ON invites(active);
-- +migrate Down -- +migrate Down
DROP TABLE members; DROP TABLE members;
DROP TABLE fallback_passwords;
DROP INDEX fallback_passwords_by_login; DROP INDEX fallback_passwords_by_login;
DROP TABLE fallback_passwords;
DROP TABLE invites;
DROP INDEX invite_active_ids; DROP INDEX invite_active_ids;
DROP INDEX invite_active_tokens; DROP INDEX invite_active_tokens;
DROP INDEX invite_inactive; DROP INDEX invite_inactive;
DROP TABLE invites;
DROP TABLE aliases;
DROP INDEX aliases_ids; DROP INDEX aliases_ids;
DROP INDEX aliases_names; DROP INDEX aliases_names;
DROP TABLE aliases;
DROP TABLE denied_keys; DROP INDEX denied_keys_by_pubkey;
DROP INDEX denied_keys_by_pubkey; DROP TABLE denied_keys;

View File

@ -12,6 +12,6 @@ CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token);
CREATE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id); CREATE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id);
-- +migrate Down -- +migrate Down
DROP TABLE SIWSSB_sessions;
DROP INDEX SIWSSB_by_token; DROP INDEX SIWSSB_by_token;
DROP INDEX SIWSSB_by_member; DROP INDEX SIWSSB_by_member;
DROP TABLE SIWSSB_sessions;

View File

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

View File

@ -4,25 +4,27 @@
package models package models
var TableNames = struct { var TableNames = struct {
SIWSSBSessions string SIWSSBSessions string
Aliases string Aliases string
Config string Config string
DeniedKeys string DeniedKeys string
FallbackPasswords string FallbackPasswords string
Invites string FallbackResetTokens string
Members string Invites string
Notices string Members string
PinNotices string Notices string
Pins string PinNotices string
Pins string
}{ }{
SIWSSBSessions: "SIWSSB_sessions", SIWSSBSessions: "SIWSSB_sessions",
Aliases: "aliases", Aliases: "aliases",
Config: "config", Config: "config",
DeniedKeys: "denied_keys", DeniedKeys: "denied_keys",
FallbackPasswords: "fallback_passwords", FallbackPasswords: "fallback_passwords",
Invites: "invites", FallbackResetTokens: "fallback_reset_tokens",
Members: "members", Invites: "invites",
Notices: "notices", Members: "members",
PinNotices: "pin_notices", Notices: "notices",
Pins: "pins", PinNotices: "pin_notices",
Pins: "pins",
} }

View File

@ -23,7 +23,6 @@ import (
// FallbackPassword is an object representing the database table. // FallbackPassword is an object representing the database table.
type FallbackPassword struct { type FallbackPassword struct {
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"` 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"` 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"` MemberID int64 `boil:"member_id" json:"member_id" toml:"member_id" yaml:"member_id"`
@ -33,12 +32,10 @@ type FallbackPassword struct {
var FallbackPasswordColumns = struct { var FallbackPasswordColumns = struct {
ID string ID string
Login string
PasswordHash string PasswordHash string
MemberID string MemberID string
}{ }{
ID: "id", ID: "id",
Login: "login",
PasswordHash: "password_hash", PasswordHash: "password_hash",
MemberID: "member_id", MemberID: "member_id",
} }
@ -47,12 +44,10 @@ var FallbackPasswordColumns = struct {
var FallbackPasswordWhere = struct { var FallbackPasswordWhere = struct {
ID whereHelperint64 ID whereHelperint64
Login whereHelperstring
PasswordHash whereHelper__byte PasswordHash whereHelper__byte
MemberID whereHelperint64 MemberID whereHelperint64
}{ }{
ID: whereHelperint64{field: "\"fallback_passwords\".\"id\""}, ID: whereHelperint64{field: "\"fallback_passwords\".\"id\""},
Login: whereHelperstring{field: "\"fallback_passwords\".\"login\""},
PasswordHash: whereHelper__byte{field: "\"fallback_passwords\".\"password_hash\""}, PasswordHash: whereHelper__byte{field: "\"fallback_passwords\".\"password_hash\""},
MemberID: whereHelperint64{field: "\"fallback_passwords\".\"member_id\""}, MemberID: whereHelperint64{field: "\"fallback_passwords\".\"member_id\""},
} }
@ -78,9 +73,9 @@ func (*fallbackPasswordR) NewStruct() *fallbackPasswordR {
type fallbackPasswordL struct{} type fallbackPasswordL struct{}
var ( var (
fallbackPasswordAllColumns = []string{"id", "login", "password_hash", "member_id"} fallbackPasswordAllColumns = []string{"id", "password_hash", "member_id"}
fallbackPasswordColumnsWithoutDefault = []string{} fallbackPasswordColumnsWithoutDefault = []string{}
fallbackPasswordColumnsWithDefault = []string{"id", "login", "password_hash", "member_id"} fallbackPasswordColumnsWithDefault = []string{"id", "password_hash", "member_id"}
fallbackPasswordPrimaryKeyColumns = []string{"id"} fallbackPasswordPrimaryKeyColumns = []string{"id"}
) )
@ -457,7 +452,7 @@ func (fallbackPasswordL) LoadMember(ctx context.Context, e boil.ContextExecutor,
if foreign.R == nil { if foreign.R == nil {
foreign.R = &memberR{} foreign.R = &memberR{}
} }
foreign.R.FallbackPasswords = append(foreign.R.FallbackPasswords, object) foreign.R.FallbackPassword = object
return nil return nil
} }
@ -468,7 +463,7 @@ func (fallbackPasswordL) LoadMember(ctx context.Context, e boil.ContextExecutor,
if foreign.R == nil { if foreign.R == nil {
foreign.R = &memberR{} foreign.R = &memberR{}
} }
foreign.R.FallbackPasswords = append(foreign.R.FallbackPasswords, local) foreign.R.FallbackPassword = local
break break
} }
} }
@ -479,7 +474,7 @@ func (fallbackPasswordL) LoadMember(ctx context.Context, e boil.ContextExecutor,
// SetMember of the fallbackPassword to the related item. // SetMember of the fallbackPassword to the related item.
// Sets o.R.Member to related. // 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 { func (o *FallbackPassword) SetMember(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Member) error {
var err error var err error
if insert { if insert {
@ -515,10 +510,10 @@ func (o *FallbackPassword) SetMember(ctx context.Context, exec boil.ContextExecu
if related.R == nil { if related.R == nil {
related.R = &memberR{ related.R = &memberR{
FallbackPasswords: FallbackPasswordSlice{o}, FallbackPassword: o,
} }
} else { } else {
related.R.FallbackPasswords = append(related.R.FallbackPasswords, o) related.R.FallbackPassword = o
} }
return nil return nil

File diff suppressed because it is too large Load Diff

View File

@ -55,23 +55,29 @@ var MemberWhere = struct {
// MemberRels is where relationship names are stored. // MemberRels is where relationship names are stored.
var MemberRels = struct { var MemberRels = struct {
SIWSSBSessions string FallbackPassword string
Aliases string SIWSSBSessions string
FallbackPasswords string Aliases string
CreatedByInvites string ForMemberFallbackResetTokens string
CreatedByFallbackResetTokens string
CreatedByInvites string
}{ }{
SIWSSBSessions: "SIWSSBSessions", FallbackPassword: "FallbackPassword",
Aliases: "Aliases", SIWSSBSessions: "SIWSSBSessions",
FallbackPasswords: "FallbackPasswords", Aliases: "Aliases",
CreatedByInvites: "CreatedByInvites", ForMemberFallbackResetTokens: "ForMemberFallbackResetTokens",
CreatedByFallbackResetTokens: "CreatedByFallbackResetTokens",
CreatedByInvites: "CreatedByInvites",
} }
// memberR is where relationships are stored. // memberR is where relationships are stored.
type memberR struct { type memberR struct {
SIWSSBSessions SIWSSBSessionSlice `boil:"SIWSSBSessions" json:"SIWSSBSessions" toml:"SIWSSBSessions" yaml:"SIWSSBSessions"` FallbackPassword *FallbackPassword `boil:"FallbackPassword" json:"FallbackPassword" toml:"FallbackPassword" yaml:"FallbackPassword"`
Aliases AliasSlice `boil:"Aliases" json:"Aliases" toml:"Aliases" yaml:"Aliases"` SIWSSBSessions SIWSSBSessionSlice `boil:"SIWSSBSessions" json:"SIWSSBSessions" toml:"SIWSSBSessions" yaml:"SIWSSBSessions"`
FallbackPasswords FallbackPasswordSlice `boil:"FallbackPasswords" json:"FallbackPasswords" toml:"FallbackPasswords" yaml:"FallbackPasswords"` Aliases AliasSlice `boil:"Aliases" json:"Aliases" toml:"Aliases" yaml:"Aliases"`
CreatedByInvites InviteSlice `boil:"CreatedByInvites" json:"CreatedByInvites" toml:"CreatedByInvites" yaml:"CreatedByInvites"` 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 // 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 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. // SIWSSBSessions retrieves all the SIWSSB_session's SIWSSBSessions with an executor.
func (o *Member) SIWSSBSessions(mods ...qm.QueryMod) sIWSSBSessionQuery { func (o *Member) SIWSSBSessions(mods ...qm.QueryMod) sIWSSBSessionQuery {
var queryMods []qm.QueryMod var queryMods []qm.QueryMod
@ -406,22 +426,43 @@ func (o *Member) Aliases(mods ...qm.QueryMod) aliasQuery {
return query return query
} }
// FallbackPasswords retrieves all the fallback_password's FallbackPasswords with an executor. // ForMemberFallbackResetTokens retrieves all the fallback_reset_token's FallbackResetTokens with an executor via for_member column.
func (o *Member) FallbackPasswords(mods ...qm.QueryMod) fallbackPasswordQuery { func (o *Member) ForMemberFallbackResetTokens(mods ...qm.QueryMod) fallbackResetTokenQuery {
var queryMods []qm.QueryMod var queryMods []qm.QueryMod
if len(mods) != 0 { if len(mods) != 0 {
queryMods = append(queryMods, mods...) queryMods = append(queryMods, mods...)
} }
queryMods = append(queryMods, queryMods = append(queryMods,
qm.Where("\"fallback_passwords\".\"member_id\"=?", o.ID), qm.Where("\"fallback_reset_tokens\".\"for_member\"=?", o.ID),
) )
query := FallbackPasswords(queryMods...) query := FallbackResetTokens(queryMods...)
queries.SetFrom(query.Query, "\"fallback_passwords\"") queries.SetFrom(query.Query, "\"fallback_reset_tokens\"")
if len(queries.GetSelect(query.Query)) == 0 { 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 return query
@ -448,6 +489,107 @@ func (o *Member) CreatedByInvites(mods ...qm.QueryMod) inviteQuery {
return query 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 // 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. // 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 { 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 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. // 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 slice []*Member
var object *Member var object *Member
@ -684,8 +826,8 @@ func (memberL) LoadFallbackPasswords(ctx context.Context, e boil.ContextExecutor
} }
query := NewQuery( query := NewQuery(
qm.From(`fallback_passwords`), qm.From(`fallback_reset_tokens`),
qm.WhereIn(`fallback_passwords.member_id in ?`, args...), qm.WhereIn(`fallback_reset_tokens.for_member in ?`, args...),
) )
if mods != nil { if mods != nil {
mods.Apply(query) mods.Apply(query)
@ -693,22 +835,22 @@ func (memberL) LoadFallbackPasswords(ctx context.Context, e boil.ContextExecutor
results, err := query.QueryContext(ctx, e) results, err := query.QueryContext(ctx, e)
if err != nil { 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 { 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 { 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 { 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 { for _, obj := range resultSlice {
if err := obj.doAfterSelectHooks(ctx, e); err != nil { if err := obj.doAfterSelectHooks(ctx, e); err != nil {
return err return err
@ -716,24 +858,122 @@ func (memberL) LoadFallbackPasswords(ctx context.Context, e boil.ContextExecutor
} }
} }
if singular { if singular {
object.R.FallbackPasswords = resultSlice object.R.ForMemberFallbackResetTokens = resultSlice
for _, foreign := range resultSlice { for _, foreign := range resultSlice {
if foreign.R == nil { if foreign.R == nil {
foreign.R = &fallbackPasswordR{} foreign.R = &fallbackResetTokenR{}
} }
foreign.R.Member = object foreign.R.ForMemberMember = object
} }
return nil return nil
} }
for _, foreign := range resultSlice { for _, foreign := range resultSlice {
for _, local := range slice { for _, local := range slice {
if local.ID == foreign.MemberID { if local.ID == foreign.ForMember {
local.R.FallbackPasswords = append(local.R.FallbackPasswords, foreign) local.R.ForMemberFallbackResetTokens = append(local.R.ForMemberFallbackResetTokens, foreign)
if foreign.R == nil { 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 break
} }
} }
@ -840,6 +1080,57 @@ func (memberL) LoadCreatedByInvites(ctx context.Context, e boil.ContextExecutor,
return nil 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 // AddSIWSSBSessions adds the given related objects to the existing relationships
// of the member, optionally inserting them as new records. // of the member, optionally inserting them as new records.
// Appends related to o.R.SIWSSBSessions. // Appends related to o.R.SIWSSBSessions.
@ -946,23 +1237,23 @@ func (o *Member) AddAliases(ctx context.Context, exec boil.ContextExecutor, inse
return nil 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. // of the member, optionally inserting them as new records.
// Appends related to o.R.FallbackPasswords. // Appends related to o.R.ForMemberFallbackResetTokens.
// Sets related.R.Member appropriately. // Sets related.R.ForMemberMember appropriately.
func (o *Member) AddFallbackPasswords(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*FallbackPassword) error { func (o *Member) AddForMemberFallbackResetTokens(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*FallbackResetToken) error {
var err error var err error
for _, rel := range related { for _, rel := range related {
if insert { if insert {
rel.MemberID = o.ID rel.ForMember = o.ID
if err = rel.Insert(ctx, exec, boil.Infer()); err != nil { if err = rel.Insert(ctx, exec, boil.Infer()); err != nil {
return errors.Wrap(err, "failed to insert into foreign table") return errors.Wrap(err, "failed to insert into foreign table")
} }
} else { } else {
updateQuery := fmt.Sprintf( updateQuery := fmt.Sprintf(
"UPDATE \"fallback_passwords\" SET %s WHERE %s", "UPDATE \"fallback_reset_tokens\" SET %s WHERE %s",
strmangle.SetParamNames("\"", "\"", 0, []string{"member_id"}), strmangle.SetParamNames("\"", "\"", 0, []string{"for_member"}),
strmangle.WhereClause("\"", "\"", 0, fallbackPasswordPrimaryKeyColumns), strmangle.WhereClause("\"", "\"", 0, fallbackResetTokenPrimaryKeyColumns),
) )
values := []interface{}{o.ID, rel.ID} 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") return errors.Wrap(err, "failed to update foreign table")
} }
rel.MemberID = o.ID rel.ForMember = o.ID
} }
} }
if o.R == nil { if o.R == nil {
o.R = &memberR{ o.R = &memberR{
FallbackPasswords: related, ForMemberFallbackResetTokens: related,
} }
} else { } else {
o.R.FallbackPasswords = append(o.R.FallbackPasswords, related...) o.R.ForMemberFallbackResetTokens = append(o.R.ForMemberFallbackResetTokens, related...)
} }
for _, rel := range related { for _, rel := range related {
if rel.R == nil { if rel.R == nil {
rel.R = &fallbackPasswordR{ rel.R = &fallbackResetTokenR{
Member: o, ForMemberMember: o,
} }
} else { } 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 return nil

View File

@ -82,11 +82,19 @@ func Open(r repo.Interface) (*Database, error) {
return nil, err 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 go func() { // server might not restart as often
threeDays := 5 * 24 * time.Hour fiveDays := 5 * 24 * time.Hour
ticker := time.NewTicker(threeDays) ticker := time.NewTicker(fiveDays)
for range ticker.C { for range ticker.C {
err := transact(db, func(tx *sql.Tx) error { err := transact(db, func(tx *sql.Tx) error {
if err := deleteConsumedResetTokens(tx); err != nil {
return err
}
return deleteConsumedInvites(tx) return deleteConsumedInvites(tx)
}) })
if err != nil { if err != nil {

View File

@ -1,8 +1,6 @@
package sqlite package sqlite
import ( import (
"bytes"
"context"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -11,8 +9,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo" "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 // verify the database opens and migrates successfully from zero state
@ -28,25 +24,3 @@ func TestSchema(t *testing.T) {
err = db.Close() err = db.Close()
require.NoError(t, err) 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)
}

View File

@ -47,11 +47,13 @@ var HTMLTemplates = []string{
"admin/member.tmpl", "admin/member.tmpl",
"admin/member-list.tmpl", "admin/member-list.tmpl",
"admin/members-remove-confirm.tmpl", "admin/members-remove-confirm.tmpl",
"admin/members-show-password-reset-token.tmpl",
} }
// Databases is an option struct that encapsulates the required database services // Databases is an option struct that encapsulates the required database services
type Databases struct { type Databases struct {
Aliases roomdb.AliasesService Aliases roomdb.AliasesService
AuthFallback roomdb.AuthFallbackService
Config roomdb.RoomConfig Config roomdb.RoomConfig
DeniedKeys roomdb.DeniedKeysService DeniedKeys roomdb.DeniedKeysService
Invites roomdb.InvitesService Invites roomdb.InvitesService
@ -125,6 +127,8 @@ func Handler(
netInfo: netInfo, netInfo: netInfo,
db: dbs.Members, db: dbs.Members,
fallbackAuthDB: dbs.AuthFallback,
} }
mux.HandleFunc("/member", r.HTML("admin/member.tmpl", mh.details)) mux.HandleFunc("/member", r.HTML("admin/member.tmpl", mh.details))
mux.HandleFunc("/members", r.HTML("admin/member-list.tmpl", mh.overview)) 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/change-role", mh.changeRole)
mux.HandleFunc("/members/remove/confirm", r.HTML("admin/members-remove-confirm.tmpl", mh.removeConfirm)) mux.HandleFunc("/members/remove/confirm", r.HTML("admin/members-remove-confirm.tmpl", mh.removeConfirm))
mux.HandleFunc("/members/remove", mh.remove) 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{ var ih = invitesHandler{
r: r, r: r,

View File

@ -27,7 +27,8 @@ type membersHandler struct {
urlTo web.URLMaker urlTo web.URLMaker
netInfo network.ServerEndpointDetails netInfo network.ServerEndpointDetails
db roomdb.MembersService db roomdb.MembersService
fallbackAuthDB roomdb.AuthFallbackService
} }
const redirectToMembers = "/admin/members" 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) 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
}

View File

@ -291,6 +291,7 @@ func New(
locHelper, locHelper,
admin.Databases{ admin.Databases{
Aliases: dbs.Aliases, Aliases: dbs.Aliases,
AuthFallback: dbs.AuthFallback,
Config: dbs.Config, Config: dbs.Config,
DeniedKeys: dbs.DeniedKeys, DeniedKeys: dbs.DeniedKeys,
Invites: dbs.Invites, Invites: dbs.Invites,
@ -302,7 +303,8 @@ func New(
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler)) mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
m.Get(router.MembersChangePasswordForm).HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 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) r.Error(w, req, http.StatusUnauthorized, weberrs.ErrNotAuthorized)
return return
} }
@ -316,7 +318,7 @@ func New(
return return
} }
// TODO: add resetToken to render pageData["ResetToken"] = resetToken
err = r.Render(w, req, "change-member-password.tmpl", http.StatusOK, pageData) err = r.Render(w, req, "change-member-password.tmpl", http.StatusOK, pageData)
if err != nil { if err != nil {
@ -325,18 +327,42 @@ func New(
}) })
m.Get(router.MembersChangePassword).HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 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 == "" { if redirectURL == "" {
http.Error(w, "TODO: add correct redirect handling", http.StatusInternalServerError) http.Error(w, "TODO: add correct redirect handling", http.StatusInternalServerError)
return return
} }
err := req.ParseForm() if req.Method != http.MethodPost {
if err != nil { r.Error(w, req, http.StatusBadRequest, fmt.Errorf("expected POST method"))
r.Error(w, req, http.StatusInternalServerError, err)
return 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") repeat := req.FormValue("repeat-password")
newpw := req.FormValue("new-password") newpw := req.FormValue("new-password")
@ -364,8 +390,22 @@ func New(
return return
} }
fmt.Fprintln(w, "password looks okay!") // update the password
// TODO: update password db 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 // handle setting language

View File

@ -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." 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." AuthFallbackInstruct = "Diese Methode ist ein akzeptabler Fallback, wenn Sie einen Benutzernamen und ein Passwort haben."
AuthFallbackPasswordChangeFormTitle = "Change Password" AuthFallbackNewPassword="Neues Passwort"
AuthFallbackPasswordChangeWelcome = "Here you can change your fallback password. Make sure it's longer then 10 characters and that they match." 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 # general dashboard stuff
######################### #########################

View File

@ -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." 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." AuthFallbackInstruct = "This method is an acceptable fallback, if you have a username and password."
AuthFallbackNewPassword="New Password"
AuthFallbackRepeatPassword="Repeat Password"
AuthFallbackPasswordChangeFormTitle = "Change 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 # general dashboard stuff
######################### #########################

View File

@ -89,8 +89,9 @@
>{{i18n "AdminMemberDetailsChangePassword"}}</a> >{{i18n "AdminMemberDetailsChangePassword"}}</a>
{{ else if member_is_elevated }} {{ else if member_is_elevated }}
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsInitiatePasswordChange"}}</label> <label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsInitiatePasswordChange"}}</label>
<form method="POST" action="{{urlTo "admin:members:create-password-reset-link" "id" .Member.ID}}"> <form method="POST" action="{{urlTo "admin:members:create-password-reset-link"}}">
{{ .csrfField }} {{ .csrfField }}
<input type="hidden" name="member_id" value="{{.Member.ID}}">
<input type="submit" <input type="submit"
class="mb-8 self-start shadow rounded px-3 py-1 text-yellow-600 ring-1 ring-yellow-400 bg-white hover:bg-yellow-600 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-400 cursor-pointer" class="mb-8 self-start shadow rounded px-3 py-1 text-yellow-600 ring-1 ring-yellow-400 bg-white hover:bg-yellow-600 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-400 cursor-pointer"
value="{{i18n "AdminMemberDetailsCreatePasswordResetLink"}}"> value="{{i18n "AdminMemberDetailsCreatePasswordResetLink"}}">

View File

@ -0,0 +1,19 @@
{{ define "title" }}{{i18n "AdminMemberPasswordResetLinkCreatedTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<svg class="mt-6 w-32 h-32 text-green-300" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z" />
</svg>
<span
id="welcome"
class="mt-6 text-center"
>{{i18n "AdminMemberPasswordResetLinkCreatedTitle"}}<br />{{i18n "AdminMemberPasswordResetLinkCreatedInstruct"}}</span>
<a
id="password-reset-link"
href="{{.ResetLinkURL}}"
class="mt-6 mb-8 bg-pink-50 w-64 py-1 px-2 break-all text-pink-600 underline"
>{{.ResetLinkURL}}</a>
</div>
{{end}}

View File

@ -1,7 +1,7 @@
{{ define "title" }}{{ i18n "AuthFallbackPasswordChangeFormTitle" }}{{ end }} {{ define "title" }}{{ i18n "AuthFallbackPasswordChangeFormTitle" }}{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="flex flex-col justify-center items-center self-center max-w-lg"> <div class="flex flex-col justify-center items-center self-center max-w-lg">
<span id="welcome" class="text-center mt-8">{{i18n "AuthFallbackPasswordChangeWelcome"}}</span> <span id="welcome" class="text-center mt-8 py-10">{{i18n "AuthFallbackPasswordChangeWelcome"}}</span>
{{ template "flashes" . }} {{ template "flashes" . }}
@ -11,15 +11,22 @@
method="POST" method="POST"
class="flex flex-col items-center self-stretch" class="flex flex-col items-center self-stretch"
> >
{{.csrfField}} {{.csrfField}}
<input type="hidden" name="member-id" value="member-id"}}>
{{if ne .ResetToken ""}}
<input type="hidden" name="reset-token" value={{.ResetToken}}>
{{end}}
<label for="password">{{i18n "AuthFallbackNewPassword"}}</label>
<input <input
id="password"
type="password" type="password"
name="new-password" name="new-password"
class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent"> class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">
<input <label for="repeat">{{i18n "AuthFallbackRepeatPassword"}}</label>
<input
id="repeat"
type="password" type="password"
name="repeat-password" name="repeat-password"
class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent"> class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">