Merge pull request #211 from ssb-ngi-pointer/passwords-overhaul
Overhaul passwords
This commit is contained in:
commit
98c5a59348
|
@ -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: |
|
||||
|
|
|
@ -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: @<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.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: <executable> + -login <val> + -key <val> */
|
||||
/* 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 <username>")
|
||||
}
|
||||
|
||||
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 <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()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -15,6 +15,7 @@ require (
|
|||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472
|
||||
github.com/mattevans/pwned-passwords v0.3.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
||||
|
|
5
go.sum
5
go.sum
|
@ -264,6 +264,8 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q
|
|||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattevans/pwned-passwords v0.3.0 h1:PFUAQXHH6NVugTiQ3Uh/iUY5dUljtEmzdg2kE8a7cXI=
|
||||
github.com/mattevans/pwned-passwords v0.3.0/go.mod h1:waUnV5nlikMlUqnjQtFV+DAgFPUQNPabvMGv8NG2IaQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
|
@ -318,6 +320,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
|
|||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
|
||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
|
@ -333,6 +336,8 @@ github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736 h1:C9bEdTfu5QY+TIf
|
|||
github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
|
|
|
@ -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 string) 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 string) error
|
||||
}
|
||||
|
||||
// AuthWithSSBService defines utility functions for the challenge/response system of sign-in with ssb
|
||||
|
|
|
@ -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 int64
|
||||
}
|
||||
createResetTokenReturns struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
createResetTokenReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
SetPasswordStub func(context.Context, int64, string) error
|
||||
setPasswordMutex sync.RWMutex
|
||||
setPasswordArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 int64
|
||||
arg3 string
|
||||
arg4 []byte
|
||||
}
|
||||
createReturns struct {
|
||||
setPasswordReturns struct {
|
||||
result1 error
|
||||
}
|
||||
createReturnsOnCall map[int]struct {
|
||||
setPasswordReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
SetPasswordWithTokenStub func(context.Context, string, string) error
|
||||
setPasswordWithTokenMutex sync.RWMutex
|
||||
setPasswordWithTokenArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
arg3 string
|
||||
}
|
||||
setPasswordWithTokenReturns struct {
|
||||
result1 error
|
||||
}
|
||||
setPasswordWithTokenReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
|
@ -106,26 +133,86 @@ 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)
|
||||
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 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)
|
||||
}
|
||||
fake.createMutex.Lock()
|
||||
ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)]
|
||||
fake.createArgsForCall = append(fake.createArgsForCall, struct {
|
||||
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 string) error {
|
||||
fake.setPasswordMutex.Lock()
|
||||
ret, specificReturn := fake.setPasswordReturnsOnCall[len(fake.setPasswordArgsForCall)]
|
||||
fake.setPasswordArgsForCall = append(fake.setPasswordArgsForCall, 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()
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.SetPasswordStub
|
||||
fakeReturns := fake.setPasswordReturns
|
||||
fake.recordInvocation("SetPassword", []interface{}{arg1, arg2, arg3})
|
||||
fake.setPasswordMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3, arg4)
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
|
@ -133,44 +220,107 @@ 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, string) 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, string) {
|
||||
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 string) error {
|
||||
fake.setPasswordWithTokenMutex.Lock()
|
||||
ret, specificReturn := fake.setPasswordWithTokenReturnsOnCall[len(fake.setPasswordWithTokenArgsForCall)]
|
||||
fake.setPasswordWithTokenArgsForCall = append(fake.setPasswordWithTokenArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
arg3 string
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.SetPasswordWithTokenStub
|
||||
fakeReturns := fake.setPasswordWithTokenReturns
|
||||
fake.recordInvocation("SetPasswordWithToken", []interface{}{arg1, arg2, arg3})
|
||||
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, string) error) {
|
||||
fake.setPasswordWithTokenMutex.Lock()
|
||||
defer fake.setPasswordWithTokenMutex.Unlock()
|
||||
fake.SetPasswordWithTokenStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAuthFallbackService) SetPasswordWithTokenArgsForCall(i int) (context.Context, string, string) {
|
||||
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 +330,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
|
||||
|
|
|
@ -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
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
|
||||
func (af AuthFallback) SetPassword(ctx context.Context, memberID int64, password string) error {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auth/fallback: failed to hash password for new user")
|
||||
}
|
||||
newPasswordEntry.PasswordHash = hashed
|
||||
|
||||
err = newPasswordEntry.Insert(ctx, af.db, boil.Infer())
|
||||
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")
|
||||
}
|
||||
|
||||
// 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 string) error {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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 := "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 := "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 := "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, "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, 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")
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
DROP INDEX denied_keys_by_pubkey;
|
||||
DROP TABLE denied_keys;
|
|
@ -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;
|
||||
DROP INDEX SIWSSB_by_member;
|
||||
DROP TABLE SIWSSB_sessions;
|
|
@ -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;
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -9,12 +9,19 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// ErrRedirect decide to not render a page during the controller
|
||||
ErrNotAuthorized = errors.New("rooms/web: not authorized")
|
||||
|
||||
ErrDenied = errors.New("rooms: this key has been banned")
|
||||
)
|
||||
|
||||
// ErrGenericLocalized is used for one-off errors that primarily are presented for the user.
|
||||
// The contained label is passed to the i18n engine for translation.
|
||||
type ErrGenericLocalized struct{ Label string }
|
||||
|
||||
func (err ErrGenericLocalized) Error() string {
|
||||
return fmt.Sprintf("rooms/web: localized error (%s)", err.Label)
|
||||
}
|
||||
|
||||
type ErrNotFound struct{ What string }
|
||||
|
||||
func (nf ErrNotFound) Error() string {
|
||||
|
@ -26,6 +33,10 @@ type ErrBadRequest struct {
|
|||
Details error
|
||||
}
|
||||
|
||||
func (err ErrBadRequest) Unwrap() error {
|
||||
return err.Details
|
||||
}
|
||||
|
||||
func (br ErrBadRequest) Error() string {
|
||||
return fmt.Sprintf("rooms/web: bad request error: %s", br.Details)
|
||||
}
|
||||
|
@ -44,8 +55,12 @@ type ErrRedirect struct {
|
|||
Reason error
|
||||
}
|
||||
|
||||
func (err ErrRedirect) Unwrap() error {
|
||||
return err.Reason
|
||||
}
|
||||
|
||||
func (err ErrRedirect) Error() string {
|
||||
return fmt.Sprintf("rooms/web: redirecting to: %s", err.Path)
|
||||
return fmt.Sprintf("rooms/web: redirecting to: %s (reason: %s)", err.Path, err.Reason)
|
||||
}
|
||||
|
||||
type PageNotFound struct{ Path string }
|
||||
|
|
|
@ -83,10 +83,10 @@ type errorTemplateData struct {
|
|||
BackURL string
|
||||
}
|
||||
|
||||
func localizeError(ih *i18n.Localizer, err error) (int, string) {
|
||||
func localizeError(ih *i18n.Localizer, err error) (int, template.HTML) {
|
||||
|
||||
// default, unlocalized message
|
||||
msg := err.Error()
|
||||
msg := template.HTML(err.Error())
|
||||
|
||||
// localize some specific error messages
|
||||
var (
|
||||
|
@ -94,6 +94,7 @@ func localizeError(ih *i18n.Localizer, err error) (int, string) {
|
|||
pnf PageNotFound
|
||||
br ErrBadRequest
|
||||
f ErrForbidden
|
||||
gl ErrGenericLocalized
|
||||
)
|
||||
|
||||
code := http.StatusInternalServerError
|
||||
|
@ -107,6 +108,9 @@ func localizeError(ih *i18n.Localizer, err error) (int, string) {
|
|||
case err == auth.ErrBadLogin:
|
||||
msg = ih.LocalizeSimple("ErrorAuthBadLogin")
|
||||
|
||||
case errors.As(err, &gl):
|
||||
msg = ih.LocalizeSimple(gl.Label)
|
||||
|
||||
case errors.Is(err, roomdb.ErrNotFound):
|
||||
code = http.StatusNotFound
|
||||
msg = ih.LocalizeSimple("ErrorNotFound")
|
||||
|
|
|
@ -3,6 +3,7 @@ package errors
|
|||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
|
@ -38,7 +39,7 @@ const (
|
|||
|
||||
type FlashMessage struct {
|
||||
Kind FlashKind
|
||||
Message string
|
||||
Message template.HTML
|
||||
}
|
||||
|
||||
// TODO: rethink error return - maybe panic() / maybe render package?
|
||||
|
|
|
@ -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,
|
||||
|
@ -164,7 +169,7 @@ func Handler(
|
|||
|
||||
// path:/ matches everything that isn't registerd (ie. its the "Not Found handler")
|
||||
mux.HandleFunc("/", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
r.Error(rw, req, 404, weberrors.ErrNotFound{What: req.URL.Path})
|
||||
r.Error(rw, req, 404, weberrors.PageNotFound{Path: req.URL.Path})
|
||||
}))
|
||||
|
||||
return customStripPrefix("/admin", mux)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
|
@ -363,3 +364,60 @@ func TestMembersRemove(t *testing.T) {
|
|||
|
||||
webassert.HasFlashMessages(t, ts.Client, listURL, "ErrorNotFound")
|
||||
}
|
||||
|
||||
func TestMembersCreateResetToken(t *testing.T) {
|
||||
ts := newSession(t)
|
||||
a := assert.New(t)
|
||||
|
||||
// setup mock
|
||||
|
||||
ts.MembersDB.GetByIDReturns(roomdb.Member{
|
||||
ID: 2342,
|
||||
Role: roomdb.RoleMember,
|
||||
PubKey: refs.FeedRef{ID: make([]byte, 32), Algo: refs.RefAlgoFeedSSB1},
|
||||
}, nil)
|
||||
|
||||
urlViewDetails := ts.URLTo(router.AdminMemberDetails, "id", "2342")
|
||||
|
||||
doc, resp := ts.Client.GetHTML(urlViewDetails)
|
||||
a.Equal(http.StatusOK, resp.Code)
|
||||
|
||||
form := doc.Find("#create-reset-token")
|
||||
a.Equal(1, form.Length(), "form missing from page")
|
||||
|
||||
formMethod, hasMethod := form.Attr("method")
|
||||
a.True(hasMethod, "missing method")
|
||||
a.Equal(http.MethodPost, formMethod, "wrong method")
|
||||
|
||||
formAction, hasAction := form.Attr("action")
|
||||
a.True(hasAction, "missing action")
|
||||
|
||||
resetURL := ts.URLTo(router.AdminMembersCreateFallbackReset)
|
||||
a.Equal(resetURL.String(), formAction, "wrong action")
|
||||
|
||||
webassert.ElementsInForm(t, form, []webassert.FormElement{
|
||||
{Name: "member_id", Value: "2342", Type: "hidden"},
|
||||
})
|
||||
|
||||
// now create the reset link
|
||||
|
||||
ts.User.Role = roomdb.RoleAdmin
|
||||
|
||||
testToken := "super-secure-token"
|
||||
ts.FallbackDB.CreateResetTokenReturns(testToken, nil)
|
||||
|
||||
resp = ts.Client.PostForm(resetURL, url.Values{
|
||||
"member_id": []string{"2342"},
|
||||
// dont need to setup csrf on admin tests
|
||||
})
|
||||
a.Equal(http.StatusOK, resp.Code)
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotResetURL, has := doc.Find("#password-reset-link").Attr("href")
|
||||
a.True(has, "should have an href")
|
||||
|
||||
wantResetURL := ts.URLTo(router.MembersChangePassword, "token", testToken)
|
||||
a.Equal(wantResetURL.String(), gotResetURL)
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ type testSession struct {
|
|||
AliasesDB *mockdb.FakeAliasesService
|
||||
ConfigDB *mockdb.FakeRoomConfig
|
||||
DeniedKeysDB *mockdb.FakeDeniedKeysService
|
||||
FallbackDB *mockdb.FakeAuthFallbackService
|
||||
InvitesDB *mockdb.FakeInvitesService
|
||||
NoticeDB *mockdb.FakeNoticesService
|
||||
MembersDB *mockdb.FakeMembersService
|
||||
|
@ -74,6 +75,7 @@ func newSession(t *testing.T) *testSession {
|
|||
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil)
|
||||
ts.ConfigDB.GetDefaultLanguageReturns("en", nil)
|
||||
ts.DeniedKeysDB = new(mockdb.FakeDeniedKeysService)
|
||||
ts.FallbackDB = new(mockdb.FakeAuthFallbackService)
|
||||
ts.MembersDB = new(mockdb.FakeMembersService)
|
||||
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
|
||||
ts.NoticeDB = new(mockdb.FakeNoticesService)
|
||||
|
@ -178,6 +180,7 @@ func newSession(t *testing.T) *testSession {
|
|||
locHelper,
|
||||
Databases{
|
||||
Aliases: ts.AliasesDB,
|
||||
AuthFallback: ts.FallbackDB,
|
||||
Config: ts.ConfigDB,
|
||||
DeniedKeys: ts.DeniedKeysDB,
|
||||
Members: ts.MembersDB,
|
||||
|
|
|
@ -31,19 +31,6 @@ func TestIndex(t *testing.T) {
|
|||
a.Equal("Default Notice Content", content)
|
||||
}
|
||||
|
||||
func TestAbout(t *testing.T) {
|
||||
ts := setup(t)
|
||||
|
||||
a := assert.New(t)
|
||||
|
||||
url := ts.URLTo(router.CompleteAbout)
|
||||
|
||||
html, resp := ts.Client.GetHTML(url)
|
||||
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
|
||||
found := html.Find("h1").Text()
|
||||
a.Equal("The about page", found)
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
ts := setup(t)
|
||||
|
||||
|
|
|
@ -34,9 +34,10 @@ import (
|
|||
|
||||
var HTMLTemplates = []string{
|
||||
"landing/index.tmpl",
|
||||
"landing/about.tmpl",
|
||||
"alias.tmpl",
|
||||
|
||||
"change-member-password.tmpl",
|
||||
|
||||
"invite/consumed.tmpl",
|
||||
"invite/facade.tmpl",
|
||||
"invite/facade-fallback.tmpl",
|
||||
|
@ -285,6 +286,7 @@ func New(
|
|||
locHelper,
|
||||
admin.Databases{
|
||||
Aliases: dbs.Aliases,
|
||||
AuthFallback: dbs.AuthFallback,
|
||||
Config: dbs.Config,
|
||||
DeniedKeys: dbs.DeniedKeys,
|
||||
Invites: dbs.Invites,
|
||||
|
@ -295,6 +297,10 @@ func New(
|
|||
)
|
||||
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
|
||||
|
||||
var mh = newMembersHandler(netInfo.Development, r, urlTo, flashHelper, dbs.AuthFallback)
|
||||
m.Get(router.MembersChangePasswordForm).HandlerFunc(r.HTML("change-member-password.tmpl", mh.changePasswordForm))
|
||||
m.Get(router.MembersChangePassword).HandlerFunc(mh.changePassword)
|
||||
|
||||
// handle setting language
|
||||
m.Get(router.CompleteSetLanguage).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
lang := req.FormValue("lang")
|
||||
|
@ -333,7 +339,6 @@ func New(
|
|||
Language: notice.Language,
|
||||
}, nil
|
||||
}))
|
||||
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
|
||||
|
||||
// notices (the mini-CMS)
|
||||
var nh = noticeHandler{
|
||||
|
@ -372,22 +377,22 @@ func New(
|
|||
m.Get(router.CompleteInviteInsertID).Handler(r.HTML("invite/insert-id.tmpl", ih.presentInsert))
|
||||
m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume)
|
||||
|
||||
// statuc assets
|
||||
// static assets
|
||||
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
|
||||
|
||||
// TODO: doesnt work because of of mainMux wrapper, see issue #35
|
||||
m.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
eh.Handle(rw, req, http.StatusNotFound, weberrs.PageNotFound{Path: req.URL.Path})
|
||||
})
|
||||
|
||||
// hook up main stdlib mux to the gorrilla/mux with named routes
|
||||
// TODO: issue #35
|
||||
mainMux.Handle("/", m)
|
||||
|
||||
consumeURL := urlTo(router.CompleteInviteConsume)
|
||||
|
||||
// apply HTTP middleware
|
||||
middlewares := []func(http.Handler) http.Handler{
|
||||
logging.RecoveryHandler(),
|
||||
logging.InjectHandler(logger),
|
||||
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
|
||||
CSRF,
|
||||
|
||||
|
@ -403,6 +408,9 @@ func New(
|
|||
next.ServeHTTP(w, req)
|
||||
})
|
||||
},
|
||||
|
||||
logging.InjectHandler(logger),
|
||||
logging.RecoveryHandler(),
|
||||
}
|
||||
|
||||
if !web.Production {
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
hibp "github.com/mattevans/pwned-passwords"
|
||||
"go.mindeco.de/http/render"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
weberrs "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
)
|
||||
|
||||
type membersHandler struct {
|
||||
r *render.Renderer
|
||||
urlTo web.URLMaker
|
||||
fh *weberrs.FlashHelper
|
||||
|
||||
authFallbackDB roomdb.AuthFallbackService
|
||||
|
||||
leakedLookup func(string) (bool, error)
|
||||
}
|
||||
|
||||
func newMembersHandler(devMode bool, r *render.Renderer, urlTo web.URLMaker, fh *weberrs.FlashHelper, db roomdb.AuthFallbackService) membersHandler {
|
||||
mh := membersHandler{
|
||||
r: r,
|
||||
urlTo: urlTo,
|
||||
fh: fh,
|
||||
|
||||
authFallbackDB: db,
|
||||
}
|
||||
|
||||
// we dont want to need network for our tests.
|
||||
if devMode {
|
||||
mh.leakedLookup = func(_ string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
} else {
|
||||
// Init the have-i-been-pwned client for insecure password checks.
|
||||
const storeExpiry = 1 * time.Hour
|
||||
hibpClient := hibp.NewClient(storeExpiry)
|
||||
|
||||
mh.leakedLookup = hibpClient.Pwned.Compromised
|
||||
}
|
||||
|
||||
return mh
|
||||
}
|
||||
|
||||
func (mh membersHandler) changePasswordForm(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
resetToken := req.URL.Query().Get("token")
|
||||
if members.FromContext(req.Context()) == nil && resetToken == "" {
|
||||
return nil, weberrs.ErrNotAuthorized
|
||||
}
|
||||
|
||||
// you can't do anything with a wrong/guessed token
|
||||
|
||||
var pageData = make(map[string]interface{})
|
||||
pageData[csrf.TemplateTag] = csrf.TemplateField(req)
|
||||
|
||||
var err error
|
||||
pageData["Flashes"], err = mh.fh.GetAll(w, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageData["ResetToken"] = resetToken
|
||||
|
||||
return pageData, nil
|
||||
}
|
||||
|
||||
func (mh membersHandler) changePassword(w http.ResponseWriter, req *http.Request) {
|
||||
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
|
||||
}
|
||||
|
||||
if req.Method != http.MethodPost {
|
||||
mh.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("expected POST method"))
|
||||
return
|
||||
}
|
||||
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
mh.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 != "" {
|
||||
mh.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")
|
||||
|
||||
if newpw != repeat {
|
||||
mh.fh.AddError(w, req, weberrs.ErrGenericLocalized{Label: "ErrorPasswordDidntMatch"})
|
||||
http.Redirect(w, req, redirectURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if len(newpw) < 10 {
|
||||
mh.fh.AddError(w, req, weberrs.ErrGenericLocalized{Label: "ErrorPasswordTooShort"})
|
||||
http.Redirect(w, req, redirectURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
isPwned, err := mh.leakedLookup(newpw)
|
||||
if err != nil {
|
||||
mh.r.Error(w, req, http.StatusInternalServerError, fmt.Errorf("have-i-been-pwned client failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if isPwned {
|
||||
mh.fh.AddError(w, req, weberrs.ErrGenericLocalized{Label: "ErrorPasswordLeaked"})
|
||||
http.Redirect(w, req, redirectURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// update the password
|
||||
if resetToken == "" {
|
||||
err = mh.authFallbackDB.SetPassword(ctx, memberID, newpw)
|
||||
} else {
|
||||
err = mh.authFallbackDB.SetPasswordWithToken(ctx, resetToken, newpw)
|
||||
}
|
||||
|
||||
// add flash msg about the outcome and redirect the user
|
||||
if err != nil {
|
||||
mh.fh.AddError(w, req, err)
|
||||
} else {
|
||||
mh.fh.AddMessage(w, req, "AuthFallbackPasswordUpdated")
|
||||
}
|
||||
|
||||
redirectURL = mh.urlTo(router.AuthFallbackLogin).Path
|
||||
http.Redirect(w, req, redirectURL, http.StatusSeeOther)
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
|
||||
)
|
||||
|
||||
func TestLoginAndChangePassword(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a := assert.New(t)
|
||||
|
||||
signInFormURL := ts.URLTo(router.AuthFallbackLogin)
|
||||
|
||||
doc, resp := ts.Client.GetHTML(signInFormURL)
|
||||
a.Equal(http.StatusOK, resp.Code)
|
||||
|
||||
csrfCookie := resp.Result().Cookies()
|
||||
a.True(len(csrfCookie) > 0, "should have one cookie for CSRF protection validation")
|
||||
|
||||
passwordForm := doc.Find("#password-fallback")
|
||||
webassert.CSRFTokenPresent(t, passwordForm)
|
||||
|
||||
csrfTokenElem := passwordForm.Find("input[type=hidden]")
|
||||
a.Equal(1, csrfTokenElem.Length())
|
||||
|
||||
csrfName, has := csrfTokenElem.Attr("name")
|
||||
a.True(has, "should have a name attribute")
|
||||
|
||||
csrfValue, has := csrfTokenElem.Attr("value")
|
||||
a.True(has, "should have value attribute")
|
||||
|
||||
loginVals := url.Values{
|
||||
"user": []string{"test"},
|
||||
"pass": []string{"test"},
|
||||
|
||||
csrfName: []string{csrfValue},
|
||||
}
|
||||
ts.AuthFallbackDB.CheckReturns(int64(23), nil)
|
||||
ts.MembersDB.GetByIDReturns(roomdb.Member{ID: 23}, nil)
|
||||
|
||||
signInURL := ts.URLTo(router.AuthFallbackFinalize)
|
||||
|
||||
// important for CSRF
|
||||
var refererHeader = make(http.Header)
|
||||
refererHeader.Set("Referer", "https://localhost")
|
||||
ts.Client.SetHeaders(refererHeader)
|
||||
|
||||
resp = ts.Client.PostForm(signInURL, loginVals)
|
||||
a.Equal(http.StatusSeeOther, resp.Code, "wrong HTTP status code for sign in")
|
||||
|
||||
a.Equal(1, ts.AuthFallbackDB.CheckCallCount())
|
||||
|
||||
// now request the protected dashboard page
|
||||
dashboardURL := ts.URLTo(router.AdminDashboard)
|
||||
|
||||
html, resp := ts.Client.GetHTML(dashboardURL)
|
||||
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
|
||||
t.Log(html.Find("body").Text())
|
||||
}
|
||||
|
||||
// check the link to the own details is there
|
||||
gotDetailsPageURL, has := html.Find("#own-details-page").Attr("href")
|
||||
a.True(has, "did not get href for own details page")
|
||||
|
||||
wantDetailsPageURL := ts.URLTo(router.AdminMemberDetails, "id", "23")
|
||||
a.Equal(wantDetailsPageURL.String(), gotDetailsPageURL)
|
||||
|
||||
// check the details page has the link to change the password
|
||||
html, resp = ts.Client.GetHTML(wantDetailsPageURL)
|
||||
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for own details page")
|
||||
|
||||
gotChangePasswordURL, has := html.Find("#change-password").Attr("href")
|
||||
a.True(has, "did not get href for pw change page")
|
||||
|
||||
wantChangePasswordURL := ts.URLTo(router.MembersChangePasswordForm)
|
||||
a.Equal(wantChangePasswordURL.String(), gotChangePasswordURL)
|
||||
|
||||
// query the form to assert the form and get a csrf token
|
||||
html, resp = ts.Client.GetHTML(wantChangePasswordURL)
|
||||
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for change password form")
|
||||
|
||||
pwForm := html.Find("#change-password")
|
||||
|
||||
postData := webassert.CSRFTokenPresent(t, pwForm)
|
||||
|
||||
webassert.ElementsInForm(t, pwForm, []webassert.FormElement{
|
||||
{Name: "new-password", Type: "password"},
|
||||
{Name: "repeat-password", Type: "password"},
|
||||
})
|
||||
|
||||
// construct the password change request(s)
|
||||
|
||||
testPassword := "our-super-secret-new-password"
|
||||
|
||||
// first we make sure they need to match
|
||||
postData.Set("new-password", testPassword)
|
||||
postData.Set("repeat-password", testPassword+"-whoops")
|
||||
resp = ts.Client.PostForm(wantChangePasswordURL, postData)
|
||||
a.Equal(http.StatusSeeOther, resp.Code) // redirects back with a flash message
|
||||
webassert.HasFlashMessages(t, ts.Client, wantChangePasswordURL, "ErrorPasswordDidntMatch")
|
||||
a.Equal(0, ts.AuthFallbackDB.SetPasswordCallCount(), "shouldnt call database")
|
||||
|
||||
// now check it can't be too short
|
||||
postData.Set("new-password", "nope")
|
||||
postData.Set("repeat-password", "nope")
|
||||
resp = ts.Client.PostForm(wantChangePasswordURL, postData)
|
||||
a.Equal(http.StatusSeeOther, resp.Code)
|
||||
webassert.HasFlashMessages(t, ts.Client, wantChangePasswordURL, "ErrorPasswordTooShort")
|
||||
a.Equal(0, ts.AuthFallbackDB.SetPasswordCallCount(), "shouldnt call database")
|
||||
|
||||
// now check it goes through
|
||||
postData.Set("new-password", testPassword)
|
||||
postData.Set("repeat-password", testPassword)
|
||||
resp = ts.Client.PostForm(wantChangePasswordURL, postData)
|
||||
a.Equal(http.StatusSeeOther, resp.Code)
|
||||
webassert.HasFlashMessages(t, ts.Client, wantChangePasswordURL, "AuthFallbackPasswordUpdated")
|
||||
a.Equal(1, ts.AuthFallbackDB.SetPasswordCallCount(), "should have called the database")
|
||||
_, mid, pw := ts.AuthFallbackDB.SetPasswordArgsForCall(0)
|
||||
a.EqualValues(23, mid)
|
||||
a.EqualValues(testPassword, pw)
|
||||
}
|
||||
|
||||
func TestChangePasswordWithToken(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a := assert.New(t)
|
||||
|
||||
testToken := "foo-bar"
|
||||
changePasswordURL := ts.URLTo(router.MembersChangePasswordForm, "token", testToken)
|
||||
|
||||
// query the form to assert the form and get a csrf token
|
||||
html, resp := ts.Client.GetHTML(changePasswordURL)
|
||||
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for change password form")
|
||||
|
||||
pwForm := html.Find("#change-password")
|
||||
|
||||
// important for CSRF
|
||||
var refererHeader = make(http.Header)
|
||||
refererHeader.Set("Referer", "https://localhost")
|
||||
ts.Client.SetHeaders(refererHeader)
|
||||
postData := webassert.CSRFTokenPresent(t, pwForm)
|
||||
|
||||
webassert.ElementsInForm(t, pwForm, []webassert.FormElement{
|
||||
{Name: "new-password", Type: "password"},
|
||||
{Name: "repeat-password", Type: "password"},
|
||||
{Name: "reset-token", Type: "hidden", Value: testToken},
|
||||
})
|
||||
|
||||
// construct the password change request(s)
|
||||
|
||||
postData.Set("reset-token", testToken)
|
||||
|
||||
testPassword := "our-super-secret-new-password"
|
||||
|
||||
// first we make sure they need to match
|
||||
postData.Set("new-password", testPassword)
|
||||
postData.Set("repeat-password", testPassword+"-whoops")
|
||||
resp = ts.Client.PostForm(changePasswordURL, postData)
|
||||
a.Equal(http.StatusSeeOther, resp.Code) // redirects back with a flash message
|
||||
webassert.HasFlashMessages(t, ts.Client, changePasswordURL, "ErrorPasswordDidntMatch")
|
||||
a.Equal(0, ts.AuthFallbackDB.SetPasswordWithTokenCallCount(), "shouldnt call database")
|
||||
|
||||
// now check it can't be too short
|
||||
postData.Set("new-password", "nope")
|
||||
postData.Set("repeat-password", "nope")
|
||||
resp = ts.Client.PostForm(changePasswordURL, postData)
|
||||
a.Equal(http.StatusSeeOther, resp.Code)
|
||||
webassert.HasFlashMessages(t, ts.Client, changePasswordURL, "ErrorPasswordTooShort")
|
||||
a.Equal(0, ts.AuthFallbackDB.SetPasswordWithTokenCallCount(), "shouldnt call database")
|
||||
|
||||
// now check it goes through
|
||||
postData.Set("new-password", testPassword)
|
||||
postData.Set("repeat-password", testPassword)
|
||||
resp = ts.Client.PostForm(changePasswordURL, postData)
|
||||
a.Equal(http.StatusSeeOther, resp.Code)
|
||||
webassert.HasFlashMessages(t, ts.Client, changePasswordURL, "AuthFallbackPasswordUpdated")
|
||||
a.Equal(1, ts.AuthFallbackDB.SetPasswordWithTokenCallCount(), "should have called the database")
|
||||
_, gotTok, gotPassword := ts.AuthFallbackDB.SetPasswordWithTokenArgsForCall(0)
|
||||
a.EqualValues(testPassword, gotPassword)
|
||||
a.EqualValues(testToken, gotTok)
|
||||
}
|
|
@ -33,6 +33,9 @@ ErrorPageNotFound = "Die angeforderte Seite <strong> ({{.Path}}) </ strong> ist
|
|||
ErrorNotAuthorized = "Sie sind nicht autorisiert auf diese Seite zuzugreifen."
|
||||
ErrorForbidden = "Die Anforderung konnte wegen fehlender Berechtigungen ({{.Details}}) nicht ausgeführt werden."
|
||||
ErrorBadRequest = "Bei Ihrer Anfrage ist ein Problem aufgetreten: {{.Where}} ({{.Details}}"
|
||||
ErrorPasswordDidntMatch = "Die eingegebenen Passwörter sind nicht identisch."
|
||||
ErrorPasswordTooShort = "Das neue Passwort ist zu kurz. Brauche mindestens 10 Zeichen."
|
||||
ErrorPasswordLeaked = "Das neue Passwort wurde in der Liste der unsicheren Passwörter von have-i-been-pwned gefunden. Sie müssen ein anderes wählen."
|
||||
|
||||
# authentication
|
||||
################
|
||||
|
@ -55,6 +58,14 @@ 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."
|
||||
|
||||
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 <a href='https://haveibeenpwned.com'>haveibeenpwned.com</a> 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
|
||||
#########################
|
||||
|
||||
|
@ -89,6 +100,7 @@ AdminDeniedKeysTitle = "Verboten"
|
|||
AdminDeniedKeysWelcome = "Auf dieser Seite können SSB-IDs gesperrt werden, damit sie nicht mehr auf den Raum zugreifen können."
|
||||
AdminDeniedKeysAdd = "Hinzufügen"
|
||||
AdminDeniedKeysAdded = "Schlüssel wurde zur Liste hinzugefügt."
|
||||
AdminDeniedKeysRemoved = "Schlüssel wurde von der Liste entfernt."
|
||||
AdminDeniedKeysRemove = "Entfernen"
|
||||
AdminDeniedKeysComment = "Kommentar"
|
||||
AdminDeniedKeysCommentDescription = "Die Person, die dieses Verbot hinzugefügt hat, hat den folgenden Kommentar hinzugefügt"
|
||||
|
@ -101,6 +113,7 @@ AdminDeniedKeysRemoveConfirmTitle = "Mitgliederentfernung bestätigen"
|
|||
AdminMembersTitle = "Mitglieder"
|
||||
AdminMembersWelcome = "Hier sehen Sie alle Mitglieder des Raums und Möglichkeiten, neue hinzuzufügen (anhand ihrer SSB-ID) oder vorhandene zu entfernen."
|
||||
AdminMembersAdd = "Hinzufügen"
|
||||
AdminMembersSelf = "Das bist du"
|
||||
|
||||
AdminMembersRemoveConfirmTitle = "Mitgliederentfernung bestätigen"
|
||||
AdminMembersRemoveConfirmWelcome = "Sind Sie sicher, dass Sie dieses Mitglied entfernen möchten? Sie verlieren ihren Alias, wenn sie einen haben."
|
||||
|
@ -111,6 +124,9 @@ AdminMemberDetailsRole = "Berechtigungsstufe"
|
|||
AdminMemberDetailsAliases = "Aliase"
|
||||
AdminMemberDetailsAliasRevoke = "Widerrufen"
|
||||
AdminMemberDetailsAliasRevoked = "Alias wurde widerrufen"
|
||||
AdminMemberDetailsInitiatePasswordChange = "Zurücksetzen des Plan-B Passworts"
|
||||
AdminMemberDetailsChangePassword = "Passwort ändern"
|
||||
AdminMemberDetailsCreatePasswordResetLink = "Reset Link erzeugen"
|
||||
AdminMemberDetailsExclusion = "Ausschluss aus diesem Raum"
|
||||
AdminMemberDetailsRemove = "Mitglied entfernen"
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ ErrorPageNotFound = "The requested page <strong>({{.Path}})</strong> is not ther
|
|||
ErrorNotAuthorized = "You are not authorized to access this page."
|
||||
ErrorForbidden = "The request could not be executed because of lacking privileges ({{.Details}})"
|
||||
ErrorBadRequest = "There was a problem with your Request: {{.Where}} ({{.Details}}"
|
||||
ErrorPasswordDidntMatch = "The passwords you entered did not match."
|
||||
ErrorPasswordTooShort = "The new password is to short. Need at least 10 characters."
|
||||
ErrorPasswordLeaked = "The new password was found on the insecure password list of have-i-been-pwned. You need to choose a different one."
|
||||
|
||||
# TODO: might be obsolete with notices
|
||||
LandingTitle = "ohai my room"
|
||||
|
@ -62,6 +65,14 @@ 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. 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 <a href='https://haveibeenpwned.com'>haveibeenpwned.com</a> 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
|
||||
#########################
|
||||
|
||||
|
@ -109,6 +120,7 @@ AdminDeniedKeysRemoved = "The key was removed from the list and is thus no longe
|
|||
AdminMembersTitle = "Members"
|
||||
AdminMembersWelcome = "Here you can see all the members of the room and ways to add new ones (by their SSB ID) or remove exising ones."
|
||||
AdminMembersAdd = "Add"
|
||||
AdminMembersSelf = "This is you"
|
||||
|
||||
AdminMembersRemoveConfirmTitle = "Confirm member removal"
|
||||
AdminMembersRemoveConfirmWelcome = "Are you sure you want to remove this member? They will lose their alias, if they have one."
|
||||
|
@ -119,6 +131,9 @@ AdminMemberDetailsRole = "Permission level"
|
|||
AdminMemberDetailsAliases = "Aliases"
|
||||
AdminMemberDetailsAliasRevoke = "Revoke"
|
||||
AdminMemberDetailsAliasRevoked = "Alias was revoked"
|
||||
AdminMemberDetailsInitiatePasswordChange = "Re-set Fallback password"
|
||||
AdminMemberDetailsChangePassword = "Change password"
|
||||
AdminMemberDetailsCreatePasswordResetLink = "Create password reset link"
|
||||
AdminMemberDetailsExclusion = "Exclusion from this room"
|
||||
AdminMemberDetailsRemove = "Remove member"
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ package i18n
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
|
@ -248,30 +249,30 @@ func (h Helper) GetRenderFuncs() []render.Option {
|
|||
return opts
|
||||
}
|
||||
|
||||
func (l Localizer) LocalizeSimple(messageID string) string {
|
||||
func (l Localizer) LocalizeSimple(messageID string) template.HTML {
|
||||
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: messageID,
|
||||
})
|
||||
if err == nil {
|
||||
return msg
|
||||
return template.HTML(msg)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err))
|
||||
}
|
||||
|
||||
func (l Localizer) LocalizeWithData(messageID string, tplData map[string]string) string {
|
||||
func (l Localizer) LocalizeWithData(messageID string, tplData map[string]string) template.HTML {
|
||||
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: messageID,
|
||||
TemplateData: tplData,
|
||||
})
|
||||
if err == nil {
|
||||
return msg
|
||||
return template.HTML(msg)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err))
|
||||
}
|
||||
|
||||
func (l Localizer) LocalizePlurals(messageID string, pluralCount int) string {
|
||||
func (l Localizer) LocalizePlurals(messageID string, pluralCount int) template.HTML {
|
||||
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: messageID,
|
||||
PluralCount: pluralCount,
|
||||
|
@ -280,20 +281,20 @@ func (l Localizer) LocalizePlurals(messageID string, pluralCount int) string {
|
|||
},
|
||||
})
|
||||
if err == nil {
|
||||
return msg
|
||||
return template.HTML(msg)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err))
|
||||
}
|
||||
|
||||
func (l Localizer) LocalizePluralsWithData(messageID string, pluralCount int, tplData map[string]string) string {
|
||||
func (l Localizer) LocalizePluralsWithData(messageID string, pluralCount int, tplData map[string]string) template.HTML {
|
||||
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: messageID,
|
||||
PluralCount: pluralCount,
|
||||
TemplateData: tplData,
|
||||
})
|
||||
if err == nil {
|
||||
return msg
|
||||
return template.HTML(msg)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err))
|
||||
|
|
|
@ -23,11 +23,12 @@ const (
|
|||
|
||||
AdminMemberDetails = "admin:member:details"
|
||||
|
||||
AdminMembersOverview = "admin:members:overview"
|
||||
AdminMembersAdd = "admin:members:add"
|
||||
AdminMembersChangeRole = "admin:members:change-role"
|
||||
AdminMembersRemoveConfirm = "admin:members:remove:confirm"
|
||||
AdminMembersRemove = "admin:members:remove"
|
||||
AdminMembersOverview = "admin:members:overview"
|
||||
AdminMembersAdd = "admin:members:add"
|
||||
AdminMembersChangeRole = "admin:members:change-role"
|
||||
AdminMembersCreateFallbackReset = "admin:members:create-password-reset-link"
|
||||
AdminMembersRemoveConfirm = "admin:members:remove:confirm"
|
||||
AdminMembersRemove = "admin:members:remove"
|
||||
|
||||
AdminInvitesOverview = "admin:invites:overview"
|
||||
AdminInvitesRevokeConfirm = "admin:invites:revoke:confirm"
|
||||
|
@ -67,6 +68,7 @@ func Admin(m *mux.Router) *mux.Router {
|
|||
m.Path("/members").Methods("GET").Name(AdminMembersOverview)
|
||||
m.Path("/members/add").Methods("POST").Name(AdminMembersAdd)
|
||||
m.Path("/members/change-role").Methods("POST").Name(AdminMembersChangeRole)
|
||||
m.Path("/members/create-fallback-reset-link").Methods("POST").Name(AdminMembersCreateFallbackReset)
|
||||
m.Path("/members/remove/confirm").Methods("GET").Name(AdminMembersRemoveConfirm)
|
||||
m.Path("/members/remove").Methods("POST").Name(AdminMembersRemove)
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
// constant names for the named routes
|
||||
const (
|
||||
CompleteIndex = "complete:index"
|
||||
CompleteAbout = "complete:about"
|
||||
|
||||
CompleteNoticeShow = "complete:notice:show"
|
||||
CompleteNoticeList = "complete:notice:list"
|
||||
|
@ -22,6 +21,9 @@ const (
|
|||
CompleteInviteFacadeFallback = "complete:invite:accept:fallback"
|
||||
CompleteInviteInsertID = "complete:invite:insert-id"
|
||||
CompleteInviteConsume = "complete:invite:consume"
|
||||
|
||||
MembersChangePasswordForm = "members:change-password:form"
|
||||
MembersChangePassword = "members:change-password"
|
||||
)
|
||||
|
||||
// CompleteApp constructs a mux.Router containing the routes for batch Complete html frontend
|
||||
|
@ -32,10 +34,12 @@ func CompleteApp() *mux.Router {
|
|||
Admin(m.PathPrefix("/admin").Subrouter())
|
||||
|
||||
m.Path("/").Methods("GET").Name(CompleteIndex)
|
||||
m.Path("/about").Methods("GET").Name(CompleteAbout)
|
||||
|
||||
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
||||
|
||||
m.Path("/members/change-password").Methods("GET").Name(MembersChangePasswordForm)
|
||||
m.Path("/members/change-password").Methods("POST").Name(MembersChangePassword)
|
||||
|
||||
m.Path("/join").Methods("GET").Name(CompleteInviteFacade)
|
||||
m.Path("/join-fallback").Methods("GET").Name(CompleteInviteFacadeFallback)
|
||||
m.Path("/join-manually").Methods("GET").Name(CompleteInviteInsertID)
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
class="text-lg font-bold my-2 ml-4"
|
||||
>{{i18npl "MemberCount" .Count}}</p>
|
||||
|
||||
{{ $self := is_logged_in }}
|
||||
<ul id="theList" class="pb-4">
|
||||
{{range $index, $member := .Entries}}
|
||||
<li class="odd:bg-gray-100 rounded-lg relative z-0 hover:z-10 hover:bg-white hover:shadow-md">
|
||||
|
@ -48,13 +49,16 @@
|
|||
<span class="font-mono truncate text-gray-600 group-hover:text-gray-800">{{$member.PubKey.Ref}}</span>
|
||||
|
||||
<div class="inline-block h-6">
|
||||
{{if eq .ID $self.ID}}
|
||||
<span class="mr-1 text-green-800 bg-green-100 rounded-lg px-2">{{i18n "AdminMembersSelf"}}</span>
|
||||
{{end}}
|
||||
{{range $member.Aliases}}
|
||||
<span class="mr-1 text-purple-800 bg-purple-100 rounded-lg px-2">{{.Name}}</span>
|
||||
{{end}}
|
||||
{{if eq .Role.String "RoleModerator"}}
|
||||
<span
|
||||
data-role="moderator"
|
||||
class="text-green-800 bg-green-100 border-green-800 rounded-lg px-2"
|
||||
class="text-blue-800 bg-blue-100 border-blue-800 rounded-lg px-2"
|
||||
>{{i18n "RoleModerator"}}</span>
|
||||
{{else if eq .Role.String "RoleAdmin"}}
|
||||
<span
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<label class="mt-2 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsRole"}}</label>
|
||||
{{ $user := is_logged_in }}
|
||||
{{ $aliasBelongsToUser := eq $user.PubKey.Ref .Member.PubKey.Ref }}
|
||||
{{ $viewerIsSameAsMember := eq $user.PubKey.Ref .Member.PubKey.Ref }}
|
||||
{{ if member_is_elevated }}
|
||||
<details class="mb-8 self-start w-40" id="change-role">
|
||||
<summary class="px-3 py-1 rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
|
@ -69,7 +69,7 @@
|
|||
>{{.Name}}</a>
|
||||
</div>
|
||||
|
||||
{{ if or member_is_elevated $aliasBelongsToUser }}
|
||||
{{ if or member_is_elevated $viewerIsSameAsMember }}
|
||||
<a
|
||||
href="{{urlTo "admin:aliases:revoke:confirm" "id" .ID}}"
|
||||
class="w-20 py-2 text-sm text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
|
||||
|
@ -79,6 +79,28 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{ if $viewerIsSameAsMember }}
|
||||
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsInitiatePasswordChange"}}</label>
|
||||
<a
|
||||
id="change-password"
|
||||
href="{{urlTo "members:change-password:form"}}"
|
||||
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"
|
||||
>{{i18n "AdminMemberDetailsChangePassword"}}</a>
|
||||
{{ else if member_is_elevated }}
|
||||
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsInitiatePasswordChange"}}</label>
|
||||
<form
|
||||
id="create-reset-token"
|
||||
method="POST"
|
||||
action="{{urlTo "admin:members:create-password-reset-link"}}">
|
||||
{{ .csrfField }}
|
||||
<input type="hidden" name="member_id" value="{{.Member.ID}}">
|
||||
<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"
|
||||
value="{{i18n "AdminMemberDetailsCreatePasswordResetLink"}}">
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ if member_is_elevated }}
|
||||
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsExclusion"}}</label>
|
||||
<a
|
||||
|
|
|
@ -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}}
|
|
@ -32,10 +32,13 @@
|
|||
</div>
|
||||
<span class="flex flex-row items-center divide-x divide-gray-300">
|
||||
<div class="flex flex-row items-center pr-3">
|
||||
<svg class="text-green-500 w-4 h-4 mr-1" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M22,18V22H18V19H15V16H12L9.74,13.74C9.19,13.91 8.61,14 8,14A6,6 0 0,1 2,8A6,6 0 0,1 8,2A6,6 0 0,1 14,8C14,8.61 13.91,9.19 13.74,9.74L22,18M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5Z" />
|
||||
</svg>
|
||||
<span class="text-green-500 text-sm truncate w-32">{{$user.PubKey.Ref}}</span>
|
||||
<div class="text-green-500">
|
||||
<a id="own-details-page" href="{{urlTo "admin:member:details" "id" $user.ID}}">
|
||||
<svg class="text-green-500 w-4 h-4 mr-1 float-left" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M22,18V22H18V19H15V16H12L9.74,13.74C9.19,13.91 8.61,14 8,14A6,6 0 0,1 2,8A6,6 0 0,1 8,2A6,6 0 0,1 14,8C14,8.61 13.91,9.19 13.74,9.74L22,18M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5Z" />
|
||||
</svg><div class="float-right text-sm truncate w-32">{{$user.PubKey.Ref}}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="{{urlTo "auth:logout"}}"
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
{{ define "title" }}{{ i18n "AuthFallbackPasswordChangeFormTitle" }}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<span id="welcome" class="text-center mt-8 py-10">{{i18n "AuthFallbackPasswordChangeWelcome"}}</span>
|
||||
|
||||
{{ template "flashes" . }}
|
||||
|
||||
<form
|
||||
id="change-password"
|
||||
action="{{urlTo "members:change-password"}}"
|
||||
method="POST"
|
||||
class="flex flex-col items-center self-stretch"
|
||||
>
|
||||
{{.csrfField}}
|
||||
|
||||
{{if ne .ResetToken ""}}
|
||||
<input type="hidden" name="reset-token" value={{.ResetToken}}>
|
||||
{{end}}
|
||||
|
||||
<label
|
||||
for="password"
|
||||
class="mt-2 mb-1 font-bold text-gray-400 text-sm"
|
||||
>{{i18n "AuthFallbackNewPassword"}}</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
name="new-password"
|
||||
class="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">
|
||||
|
||||
<label
|
||||
for="repeat"
|
||||
class="mt-2 mb-1 font-bold text-gray-400 text-sm"
|
||||
>{{i18n "AuthFallbackRepeatPassword"}}</label>
|
||||
<input
|
||||
id="repeat"
|
||||
type="password"
|
||||
name="repeat-password"
|
||||
class="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">
|
||||
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="my-8 w-32 shadow rounded px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
|
||||
>{{i18n "GenericSubmit"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -1,7 +0,0 @@
|
|||
{{ define "title" }}Landing - About{{ end }}
|
||||
{{ define "content" }}
|
||||
<div>
|
||||
<h1>The about page</h1>
|
||||
</div>
|
||||
</div> <!-- /row -->
|
||||
{{end}}
|
|
@ -26,13 +26,20 @@ func Localized(t *testing.T, html *goquery.Document, elems []LocalizedElement) {
|
|||
}
|
||||
}
|
||||
|
||||
func CSRFTokenPresent(t *testing.T, sel *goquery.Selection) {
|
||||
// CSRFTokenPresent checks a CSRF token is in side the passed selection (ususally a form).
|
||||
// The function returns a url.Values map with the token, which can be used to craft further requests.
|
||||
func CSRFTokenPresent(t *testing.T, sel *goquery.Selection) url.Values {
|
||||
a := assert.New(t)
|
||||
csrfField := sel.Find("input[name='gorilla.csrf.Token']")
|
||||
a.EqualValues(1, csrfField.Length(), "no csrf-token input tag")
|
||||
tipe, ok := csrfField.Attr("type")
|
||||
a.True(ok, "csrf input has a type")
|
||||
a.Equal("hidden", tipe, "wrong type on csrf field")
|
||||
val, ok := csrfField.Attr("value")
|
||||
a.True(ok, "should have a value")
|
||||
return url.Values{
|
||||
"gorilla.csrf.Token": []string{val},
|
||||
}
|
||||
}
|
||||
|
||||
type FormElement struct {
|
||||
|
|
Loading…
Reference in New Issue