Register endpoint handles "verified" status
Based on the verification mode specified in env. The db doesn't do anything with it yet.
This commit is contained in:
parent
832778ffd1
commit
55db62e2f9
4 changed files with 180 additions and 15 deletions
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/lbry-id/auth"
|
||||||
|
"lbryio/lbry-id/env"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/lbry-id/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +16,10 @@ type RegisterRequest struct {
|
||||||
ClientSaltSeed auth.ClientSaltSeed `json:"clientSaltSeed"`
|
ClientSaltSeed auth.ClientSaltSeed `json:"clientSaltSeed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RegisterResponse struct {
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RegisterRequest) validate() error {
|
func (r *RegisterRequest) validate() error {
|
||||||
if !r.Email.Validate() {
|
if !r.Email.Validate() {
|
||||||
return fmt.Errorf("Invalid or missing 'email'")
|
return fmt.Errorf("Invalid or missing 'email'")
|
||||||
|
@ -35,7 +40,49 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.store.CreateAccount(registerRequest.Email, registerRequest.Password, registerRequest.ClientSaltSeed)
|
verificationMode, err := env.GetAccountVerificationMode(s.env)
|
||||||
|
if err != nil {
|
||||||
|
internalServiceErrorJson(w, err, "Error getting account verification mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accountWhitelist, err := env.GetAccountWhitelist(s.env, verificationMode)
|
||||||
|
if err != nil {
|
||||||
|
internalServiceErrorJson(w, err, "Error getting account whitelist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var registerResponse RegisterResponse
|
||||||
|
|
||||||
|
modes:
|
||||||
|
switch verificationMode {
|
||||||
|
case env.AccountVerificationModeAllowAll:
|
||||||
|
// Always verified (for testers). No need to jump through email verify
|
||||||
|
// hoops.
|
||||||
|
registerResponse.Verified = true
|
||||||
|
case env.AccountVerificationModeWhitelist:
|
||||||
|
for _, whitelisteEmail := range accountWhitelist {
|
||||||
|
if whitelisteEmail == registerRequest.Email {
|
||||||
|
registerResponse.Verified = true
|
||||||
|
break modes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we have unverified users on whitelist setups, we'd need to create a way
|
||||||
|
// to verify them. It's easier to just prevent account creation. It also will
|
||||||
|
// make it easier for self-hosters to figure out that something is wrong
|
||||||
|
// with their whitelist.
|
||||||
|
errorJson(w, http.StatusForbidden, "Account not whitelisted")
|
||||||
|
return
|
||||||
|
case env.AccountVerificationModeEmailVerify:
|
||||||
|
// Not verified until they click their email link.
|
||||||
|
registerResponse.Verified = false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.store.CreateAccount(
|
||||||
|
registerRequest.Email,
|
||||||
|
registerRequest.Password,
|
||||||
|
registerRequest.ClientSaltSeed,
|
||||||
|
registerResponse.Verified,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == store.ErrDuplicateEmail || err == store.ErrDuplicateAccount {
|
if err == store.ErrDuplicateEmail || err == store.ErrDuplicateAccount {
|
||||||
|
@ -46,9 +93,7 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var registerResponse struct{} // no data to respond with, but keep it JSON
|
response, err := json.Marshal(registerResponse)
|
||||||
var response []byte
|
|
||||||
response, err = json.Marshal(registerResponse)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServiceErrorJson(w, err, "Error generating register response")
|
internalServiceErrorJson(w, err, "Error generating register response")
|
||||||
|
|
|
@ -2,10 +2,12 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -14,7 +16,10 @@ import (
|
||||||
|
|
||||||
func TestServerRegisterSuccess(t *testing.T) {
|
func TestServerRegisterSuccess(t *testing.T) {
|
||||||
testStore := &TestStore{}
|
testStore := &TestStore{}
|
||||||
s := Server{&TestAuth{}, testStore, &TestEnv{}}
|
env := map[string]string{
|
||||||
|
"ACCOUNT_VERIFICATION_MODE": "AllowAll",
|
||||||
|
}
|
||||||
|
s := Server{&TestAuth{}, testStore, &TestEnv{env}}
|
||||||
|
|
||||||
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
|
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
|
||||||
|
|
||||||
|
@ -26,11 +31,15 @@ func TestServerRegisterSuccess(t *testing.T) {
|
||||||
|
|
||||||
expectStatusCode(t, w, http.StatusCreated)
|
expectStatusCode(t, w, http.StatusCreated)
|
||||||
|
|
||||||
if string(body) != "{}" {
|
var result RegisterResponse
|
||||||
t.Errorf("Expected register response to be \"{}\": result: %+v", string(body))
|
err := json.Unmarshal(body, &result)
|
||||||
|
|
||||||
|
expectedResponse := RegisterResponse{Verified: true}
|
||||||
|
if err != nil || !reflect.DeepEqual(&result, &expectedResponse) {
|
||||||
|
t.Errorf("Unexpected value for register response. Want: %+v Got: %+v Err: %+v", expectedResponse, result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !testStore.Called.CreateAccount {
|
if testStore.Called.CreateAccount == nil {
|
||||||
t.Errorf("Expected Store.CreateAccount to be called")
|
t.Errorf("Expected Store.CreateAccount to be called")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,15 +83,19 @@ func TestServerRegisterErrors(t *testing.T) {
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
env := map[string]string{
|
||||||
|
"ACCOUNT_VERIFICATION_MODE": "AllowAll",
|
||||||
|
}
|
||||||
|
|
||||||
// Set this up to fail according to specification
|
// Set this up to fail according to specification
|
||||||
server := Server{&TestAuth{}, &TestStore{Errors: tc.storeErrors}, &TestEnv{}}
|
s := Server{&TestAuth{}, &TestStore{Errors: tc.storeErrors}, &TestEnv{env}}
|
||||||
|
|
||||||
// Make request
|
// Make request
|
||||||
requestBody := fmt.Sprintf(`{"email": "%s", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}`, tc.email)
|
requestBody := fmt.Sprintf(`{"email": "%s", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}`, tc.email)
|
||||||
req := httptest.NewRequest(http.MethodPost, PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
|
req := httptest.NewRequest(http.MethodPost, PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
server.register(w, req)
|
s.register(w, req)
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(w.Body)
|
body, _ := ioutil.ReadAll(w.Body)
|
||||||
|
|
||||||
|
@ -92,6 +105,101 @@ func TestServerRegisterErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerRegisterAccountVerification(t *testing.T) {
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
env map[string]string
|
||||||
|
expectSuccess bool
|
||||||
|
expectedVerified bool
|
||||||
|
expectedStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "allow all",
|
||||||
|
|
||||||
|
env: map[string]string{
|
||||||
|
"ACCOUNT_VERIFICATION_MODE": "AllowAll",
|
||||||
|
},
|
||||||
|
|
||||||
|
expectedVerified: true,
|
||||||
|
expectSuccess: true,
|
||||||
|
expectedStatusCode: http.StatusCreated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitelist allowed",
|
||||||
|
|
||||||
|
env: map[string]string{
|
||||||
|
"ACCOUNT_VERIFICATION_MODE": "Whitelist",
|
||||||
|
"ACCOUNT_WHITELIST": "abc@example.com",
|
||||||
|
},
|
||||||
|
|
||||||
|
expectedVerified: true,
|
||||||
|
expectSuccess: true,
|
||||||
|
expectedStatusCode: http.StatusCreated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitelist disallowed",
|
||||||
|
|
||||||
|
env: map[string]string{
|
||||||
|
"ACCOUNT_VERIFICATION_MODE": "Whitelist",
|
||||||
|
"ACCOUNT_WHITELIST": "something-else@example.com",
|
||||||
|
},
|
||||||
|
|
||||||
|
expectedVerified: false,
|
||||||
|
expectSuccess: false,
|
||||||
|
expectedStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email verify",
|
||||||
|
|
||||||
|
env: map[string]string{
|
||||||
|
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
|
||||||
|
},
|
||||||
|
|
||||||
|
expectedVerified: false,
|
||||||
|
expectSuccess: true,
|
||||||
|
expectedStatusCode: http.StatusCreated,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
testStore := &TestStore{}
|
||||||
|
s := Server{&TestAuth{}, testStore, &TestEnv{tc.env}}
|
||||||
|
|
||||||
|
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, PathRegister, bytes.NewBuffer(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.register(w, req)
|
||||||
|
body, _ := ioutil.ReadAll(w.Body)
|
||||||
|
|
||||||
|
expectStatusCode(t, w, tc.expectedStatusCode)
|
||||||
|
|
||||||
|
if tc.expectSuccess {
|
||||||
|
if testStore.Called.CreateAccount == nil {
|
||||||
|
t.Fatalf("Expected CreateAccount to be called")
|
||||||
|
}
|
||||||
|
if tc.expectedVerified != testStore.Called.CreateAccount.Verified {
|
||||||
|
t.Errorf("Unexpected value in call to CreateAccount for `verified`. Want: %+v Got: %+v", tc.expectedVerified, testStore.Called.CreateAccount.Verified)
|
||||||
|
}
|
||||||
|
var result RegisterResponse
|
||||||
|
err := json.Unmarshal(body, &result)
|
||||||
|
|
||||||
|
if err != nil || tc.expectedVerified != result.Verified {
|
||||||
|
t.Errorf("Unexpected value in register response for `verified`. Want: %+v Got: %+v Err: %+v", tc.expectedVerified, result.Verified, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if testStore.Called.CreateAccount != nil {
|
||||||
|
t.Errorf("Expected CreateAccount not to be called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerValidateRegisterRequest(t *testing.T) {
|
func TestServerValidateRegisterRequest(t *testing.T) {
|
||||||
registerRequest := RegisterRequest{Email: "joe@example.com", Password: "aoeu", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
registerRequest := RegisterRequest{Email: "joe@example.com", Password: "aoeu", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
||||||
if registerRequest.validate() != nil {
|
if registerRequest.validate() != nil {
|
||||||
|
|
|
@ -60,12 +60,19 @@ type ChangePasswordWithWalletCall struct {
|
||||||
ClientSaltSeed auth.ClientSaltSeed
|
ClientSaltSeed auth.ClientSaltSeed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateAccountCall struct {
|
||||||
|
Email auth.Email
|
||||||
|
Password auth.Password
|
||||||
|
ClientSaltSeed auth.ClientSaltSeed
|
||||||
|
Verified bool
|
||||||
|
}
|
||||||
|
|
||||||
// Whether functions are called, and sometimes what they're called with
|
// Whether functions are called, and sometimes what they're called with
|
||||||
type TestStoreFunctionsCalled struct {
|
type TestStoreFunctionsCalled struct {
|
||||||
SaveToken auth.TokenString
|
SaveToken auth.TokenString
|
||||||
GetToken auth.TokenString
|
GetToken auth.TokenString
|
||||||
GetUserId bool
|
GetUserId bool
|
||||||
CreateAccount bool
|
CreateAccount *CreateAccountCall
|
||||||
SetWallet SetWalletCall
|
SetWallet SetWalletCall
|
||||||
GetWallet bool
|
GetWallet bool
|
||||||
ChangePasswordWithWallet ChangePasswordWithWalletCall
|
ChangePasswordWithWallet ChangePasswordWithWalletCall
|
||||||
|
@ -117,8 +124,13 @@ func (s *TestStore) GetUserId(auth.Email, auth.Password) (auth.UserId, error) {
|
||||||
return 0, s.Errors.GetUserId
|
return 0, s.Errors.GetUserId
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestStore) CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed) error {
|
func (s *TestStore) CreateAccount(email auth.Email, password auth.Password, clientSaltSeed auth.ClientSaltSeed, verified bool) error {
|
||||||
s.Called.CreateAccount = true
|
s.Called.CreateAccount = &CreateAccountCall{
|
||||||
|
Email: email,
|
||||||
|
Password: password,
|
||||||
|
ClientSaltSeed: clientSaltSeed,
|
||||||
|
Verified: verified,
|
||||||
|
}
|
||||||
return s.Errors.CreateAccount
|
return s.Errors.CreateAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ type StoreInterface interface {
|
||||||
SetWallet(auth.UserId, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
|
SetWallet(auth.UserId, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
|
||||||
GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error)
|
GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error)
|
||||||
GetUserId(auth.Email, auth.Password) (auth.UserId, error)
|
GetUserId(auth.Email, auth.Password) (auth.UserId, error)
|
||||||
CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed) error
|
CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, bool) error
|
||||||
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
|
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
|
||||||
ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) error
|
ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) error
|
||||||
GetClientSaltSeed(auth.Email) (auth.ClientSaltSeed, error)
|
GetClientSaltSeed(auth.Email) (auth.ClientSaltSeed, error)
|
||||||
|
@ -359,7 +359,7 @@ func (s *Store) GetUserId(email auth.Email, password auth.Password) (userId auth
|
||||||
// Account //
|
// Account //
|
||||||
/////////////
|
/////////////
|
||||||
|
|
||||||
func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed auth.ClientSaltSeed) (err error) {
|
func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed auth.ClientSaltSeed, verified bool) (err error) {
|
||||||
key, salt, err := password.Create()
|
key, salt, err := password.Create()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in a new issue