mirror of https://github.com/grafana/grafana
Refactor: Email verification (#84393)
* Update template names * Add verifier that we can use to start verify process * Use userVerifier when verifying email on update * Add tests --------- Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>pull/84475/head
parent
38a8bf10f3
commit
8d9521fb6d
@ -1,4 +1,4 @@ |
|||||||
[[HiddenSubject .Subject "Verify your new email - [[.Name]]"]] |
[[HiddenSubject .Subject "Verify your email - [[.Name]]"]] |
||||||
|
|
||||||
Hi [[.Name]], |
Hi [[.Name]], |
||||||
|
|
@ -0,0 +1,37 @@ |
|||||||
|
package tempusertest |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
tempuser "github.com/grafana/grafana/pkg/services/temp_user" |
||||||
|
) |
||||||
|
|
||||||
|
var _ tempuser.Service = (*FakeTempUserService)(nil) |
||||||
|
|
||||||
|
type FakeTempUserService struct { |
||||||
|
tempuser.Service |
||||||
|
CreateTempUserFN func(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) |
||||||
|
ExpirePreviousVerificationsFN func(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error |
||||||
|
UpdateTempUserWithEmailSentFN func(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FakeTempUserService) CreateTempUser(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) { |
||||||
|
if f.CreateTempUserFN != nil { |
||||||
|
return f.CreateTempUserFN(ctx, cmd) |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FakeTempUserService) ExpirePreviousVerifications(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error { |
||||||
|
if f.ExpirePreviousVerificationsFN != nil { |
||||||
|
return f.ExpirePreviousVerificationsFN(ctx, cmd) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FakeTempUserService) UpdateTempUserWithEmailSent(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error { |
||||||
|
if f.UpdateTempUserWithEmailSentFN != nil { |
||||||
|
return f.UpdateTempUserWithEmailSentFN(ctx, cmd) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
package userimpl |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/notifications" |
||||||
|
tempuser "github.com/grafana/grafana/pkg/services/temp_user" |
||||||
|
"github.com/grafana/grafana/pkg/services/user" |
||||||
|
"github.com/grafana/grafana/pkg/util" |
||||||
|
) |
||||||
|
|
||||||
|
var _ user.Verifier = (*Verifier)(nil) |
||||||
|
|
||||||
|
func ProvideVerifier(us user.Service, ts tempuser.Service, ns notifications.Service) *Verifier { |
||||||
|
return &Verifier{us, ts, ns} |
||||||
|
} |
||||||
|
|
||||||
|
type Verifier struct { |
||||||
|
us user.Service |
||||||
|
ts tempuser.Service |
||||||
|
ns notifications.Service |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Verifier) VerifyEmail(ctx context.Context, cmd user.VerifyEmailCommand) error { |
||||||
|
usr, err := s.us.GetByLogin(ctx, &user.GetUserByLoginQuery{ |
||||||
|
LoginOrEmail: cmd.Email, |
||||||
|
}) |
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, user.ErrUserNotFound) { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// if email is already used by another user we stop here
|
||||||
|
if usr != nil && usr.ID != cmd.User.ID { |
||||||
|
return user.ErrEmailConflict.Errorf("email already used") |
||||||
|
} |
||||||
|
|
||||||
|
code, err := util.GetRandomString(20) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to generate verification code: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
// invalidate any pending verifications for user
|
||||||
|
if err = s.ts.ExpirePreviousVerifications( |
||||||
|
ctx, &tempuser.ExpirePreviousVerificationsCommand{InvitedByUserID: cmd.User.ID}, |
||||||
|
); err != nil { |
||||||
|
return fmt.Errorf("failed to expire previous verifications: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
tmpUsr, err := s.ts.CreateTempUser(ctx, &tempuser.CreateTempUserCommand{ |
||||||
|
OrgID: -1, |
||||||
|
// used to determine if the user was updating their email or username in the second step of the verification flow
|
||||||
|
Name: string(cmd.Action), |
||||||
|
// used to fetch the User in the second step of the verification flow
|
||||||
|
InvitedByUserID: cmd.User.ID, |
||||||
|
Email: cmd.Email, |
||||||
|
Code: code, |
||||||
|
Status: tempuser.TmpUserEmailUpdateStarted, |
||||||
|
}) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to generate temp user for email verification: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
if err := s.ns.SendVerificationEmail(ctx, ¬ifications.SendVerifyEmailCommand{ |
||||||
|
User: &cmd.User, |
||||||
|
Code: tmpUsr.Code, |
||||||
|
Email: cmd.Email, |
||||||
|
}); err != nil { |
||||||
|
return fmt.Errorf("failed to send verification email: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
if err := s.ts.UpdateTempUserWithEmailSent(ctx, &tempuser.UpdateTempUserWithEmailSentCommand{ |
||||||
|
Code: tmpUsr.Code, |
||||||
|
}); err != nil { |
||||||
|
return fmt.Errorf("failed to mark email as sent: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,108 @@ |
|||||||
|
package userimpl |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/notifications" |
||||||
|
tempuser "github.com/grafana/grafana/pkg/services/temp_user" |
||||||
|
"github.com/grafana/grafana/pkg/services/temp_user/tempusertest" |
||||||
|
"github.com/grafana/grafana/pkg/services/user" |
||||||
|
"github.com/grafana/grafana/pkg/services/user/usertest" |
||||||
|
) |
||||||
|
|
||||||
|
func TestVerifier_VerifyEmail(t *testing.T) { |
||||||
|
ts := &tempusertest.FakeTempUserService{} |
||||||
|
us := &usertest.FakeUserService{} |
||||||
|
ns := notifications.MockNotificationService() |
||||||
|
|
||||||
|
type calls struct { |
||||||
|
expireCalled bool |
||||||
|
createCalled bool |
||||||
|
updateCalled bool |
||||||
|
} |
||||||
|
|
||||||
|
verifier := ProvideVerifier(us, ts, ns) |
||||||
|
t.Run("should error if email already exist for other user", func(t *testing.T) { |
||||||
|
us.ExpectedUser = &user.User{ID: 1} |
||||||
|
err := verifier.VerifyEmail(context.Background(), user.VerifyEmailCommand{ |
||||||
|
User: user.User{ID: 2}, |
||||||
|
Email: "some@email.com", |
||||||
|
Action: user.EmailUpdateAction, |
||||||
|
}) |
||||||
|
|
||||||
|
assert.ErrorIs(t, err, user.ErrEmailConflict) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should succeed when no user has the email", func(t *testing.T) { |
||||||
|
us.ExpectedUser = nil |
||||||
|
var c calls |
||||||
|
ts.ExpirePreviousVerificationsFN = func(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error { |
||||||
|
c.expireCalled = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
ts.CreateTempUserFN = func(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) { |
||||||
|
c.createCalled = true |
||||||
|
return &tempuser.TempUser{ |
||||||
|
OrgID: cmd.OrgID, |
||||||
|
Email: cmd.Email, |
||||||
|
Name: cmd.Name, |
||||||
|
InvitedByUserID: cmd.InvitedByUserID, |
||||||
|
Code: cmd.Code, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
ts.UpdateTempUserWithEmailSentFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error { |
||||||
|
c.updateCalled = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
err := verifier.VerifyEmail(context.Background(), user.VerifyEmailCommand{ |
||||||
|
User: user.User{ID: 2}, |
||||||
|
Email: "some@email.com", |
||||||
|
Action: user.EmailUpdateAction, |
||||||
|
}) |
||||||
|
|
||||||
|
assert.ErrorIs(t, err, nil) |
||||||
|
assert.True(t, c.expireCalled) |
||||||
|
assert.True(t, c.createCalled) |
||||||
|
assert.True(t, c.updateCalled) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should succeed when the user holding the email is the same user that want to verify it", func(t *testing.T) { |
||||||
|
us.ExpectedUser = &user.User{ID: 2} |
||||||
|
var c calls |
||||||
|
ts.ExpirePreviousVerificationsFN = func(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error { |
||||||
|
c.expireCalled = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
ts.CreateTempUserFN = func(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) { |
||||||
|
c.createCalled = true |
||||||
|
return &tempuser.TempUser{ |
||||||
|
OrgID: cmd.OrgID, |
||||||
|
Email: cmd.Email, |
||||||
|
Name: cmd.Name, |
||||||
|
InvitedByUserID: cmd.InvitedByUserID, |
||||||
|
Code: cmd.Code, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
ts.UpdateTempUserWithEmailSentFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error { |
||||||
|
c.updateCalled = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
err := verifier.VerifyEmail(context.Background(), user.VerifyEmailCommand{ |
||||||
|
User: user.User{ID: 2}, |
||||||
|
Email: "some@email.com", |
||||||
|
Action: user.EmailUpdateAction, |
||||||
|
}) |
||||||
|
|
||||||
|
assert.ErrorIs(t, err, nil) |
||||||
|
assert.True(t, c.expireCalled) |
||||||
|
assert.True(t, c.createCalled) |
||||||
|
assert.True(t, c.updateCalled) |
||||||
|
}) |
||||||
|
} |
Loading…
Reference in new issue